mirror of
https://github.com/velocitatem/PHANTOM.git
synced 2026-07-15 17:43:36 +00:00
Compare commits
89 Commits
claude/imp
...
enriching-
| Author | SHA1 | Date | |
|---|---|---|---|
| 4667a1678f | |||
| a4b7b5b4b2 | |||
| 1a9901f118 | |||
| 5912062dc0 | |||
| 843564eeb0 | |||
| 9acc998cc9 | |||
| 802f31b4a1 | |||
| 66c4a0cd1d | |||
| 244af9ac09 | |||
| 76c31a2abd | |||
| 64ee7e6d9b | |||
| 1e04a928aa | |||
| 9b133cddfd | |||
| ded7290935 | |||
| 8e4dd59f90 | |||
| 024f6d4132 | |||
| 2b47c3499a | |||
| ef1d1f6557 | |||
| d7657db287 | |||
| e8229ac313 | |||
| bc6c481d03 | |||
| 895eea5674 | |||
| fba2a9739e | |||
| d1aa13360f | |||
| f6f9729424 | |||
| e22286371f | |||
| e44feb7da0 | |||
| ebd2378859 | |||
| c4d82b2ecc | |||
| a9e2e7cbf3 | |||
| e0b074161b | |||
| 08c0afb55a | |||
| c4fd1352c9 | |||
| 4abef97bf7 | |||
| 33cb0d7e95 | |||
| e8ef850089 | |||
| e7cb48e9cd | |||
|
|
dba8f3fafa | ||
|
|
9843c5deab | ||
| 13959e4b28 | |||
|
|
2f481bd94b | ||
| 72877439ca | |||
| 0f5f8affab | |||
| ee70f02a1f | |||
| 22a2c255bd | |||
| ccc19f3493 | |||
| 00e3eff2fa | |||
| 440371dba4 | |||
| b05b510f70 | |||
| 04907df393 | |||
| b2f0746c01 | |||
| 7b2d80ac4c | |||
| 0ce12fbc3b | |||
| e9cf5f0736 | |||
| 82b54428b7 | |||
| 87a35fad2c | |||
| af23d2f736 | |||
| 9cb2b0fc44 | |||
| 7c330a19c6 | |||
|
|
eb95060380 | ||
| 61dd621532 | |||
| 4c368d48f2 | |||
| 3c141a4b6c | |||
| e89cb263d4 | |||
| 62a4008c29 | |||
| 8b429b7a8e | |||
| f9bf3de71e | |||
| 131323ef56 | |||
| ec4cf074e6 | |||
| 6a06a8af4a | |||
| 3fa98f375d | |||
| 201c98bcac | |||
| 8a08458478 | |||
| 7d09232e48 | |||
| 20132c084c | |||
| 26abff5864 | |||
| 4c7d9362af | |||
| ea45801845 | |||
|
|
574e05d9e0 | ||
| 52fe865598 | |||
| 28d3f6853e | |||
| 10e8397eec | |||
| 772772b5b9 | |||
| 6e06081d60 | |||
| 83d9bb2552 | |||
| fa2aca8b13 | |||
| cd6c3d6006 | |||
|
|
b5f19e04b7 | ||
|
|
a9d73ccce5 |
48
.github/workflows/latex.yml
vendored
48
.github/workflows/latex.yml
vendored
@@ -19,10 +19,56 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
root_file: main.tex
|
root_file: main.tex
|
||||||
working_directory: paper/src
|
working_directory: paper/src
|
||||||
args: -pdf -interaction=nonstopmode -file-line-error -outdir=../build
|
args: -pdf -f -interaction=nonstopmode -file-line-error -outdir=../build
|
||||||
pre_compile: bash ../concat_code.sh
|
pre_compile: bash ../concat_code.sh
|
||||||
- name: Upload PDF
|
- name: Upload PDF
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: thesis-pdf
|
name: thesis-pdf
|
||||||
path: paper/build/main.pdf
|
path: paper/build/main.pdf
|
||||||
|
|
||||||
|
- name: Get current date
|
||||||
|
id: date
|
||||||
|
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Upload to Cloudflare R2
|
||||||
|
env:
|
||||||
|
AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
||||||
|
AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
||||||
|
AWS_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT }}
|
||||||
|
DATE: ${{ steps.date.outputs.date }}
|
||||||
|
BUCKET_NAME: ${{ secrets.R2_BUCKET_NAME }}
|
||||||
|
run: |
|
||||||
|
pip install boto3
|
||||||
|
python3 << 'EOF'
|
||||||
|
import boto3
|
||||||
|
import os
|
||||||
|
|
||||||
|
s3 = boto3.client('s3',
|
||||||
|
endpoint_url=os.environ['AWS_ENDPOINT_URL'],
|
||||||
|
aws_access_key_id=os.environ['AWS_ACCESS_KEY_ID'],
|
||||||
|
aws_secret_access_key=os.environ['AWS_SECRET_ACCESS_KEY']
|
||||||
|
)
|
||||||
|
|
||||||
|
date = os.environ['DATE']
|
||||||
|
bucket = os.environ['BUCKET_NAME']
|
||||||
|
|
||||||
|
# upload dated version
|
||||||
|
dated_filename = f"thesis-{date}.pdf"
|
||||||
|
s3.upload_file(
|
||||||
|
'paper/build/main.pdf',
|
||||||
|
bucket,
|
||||||
|
dated_filename,
|
||||||
|
ExtraArgs={'ContentType': 'application/pdf'}
|
||||||
|
)
|
||||||
|
print(f"Uploaded {dated_filename}")
|
||||||
|
|
||||||
|
# upload latest version
|
||||||
|
s3.upload_file(
|
||||||
|
'paper/build/main.pdf',
|
||||||
|
bucket,
|
||||||
|
'thesis-latest.pdf',
|
||||||
|
ExtraArgs={'ContentType': 'application/pdf'}
|
||||||
|
)
|
||||||
|
print(f"Uploaded thesis-latest.pdf")
|
||||||
|
EOF
|
||||||
|
|||||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -12,6 +12,12 @@
|
|||||||
**/_build/
|
**/_build/
|
||||||
|
|
||||||
paper/src/bib/auto
|
paper/src/bib/auto
|
||||||
|
**/_build/
|
||||||
|
paper/src/auto/*
|
||||||
|
paper/src/bib/auto
|
||||||
|
paper/template/*
|
||||||
|
docs/goals/*.md
|
||||||
|
PHANTOM.wiki/
|
||||||
experiments/airflow/logs/*
|
experiments/airflow/logs/*
|
||||||
experiments/airflow/logs/scheduler/
|
experiments/airflow/logs/scheduler/
|
||||||
experiments/airflow/logs/dag_processor_manager/
|
experiments/airflow/logs/dag_processor_manager/
|
||||||
@@ -21,4 +27,7 @@ sim/rl/behavior_loader/*.dot
|
|||||||
sim/rl/behavior_loader/*.png
|
sim/rl/behavior_loader/*.png
|
||||||
sim/rl/behavior_loader/*.svg
|
sim/rl/behavior_loader/*.svg
|
||||||
sim/rl/behavior_loader/*.pdf
|
sim/rl/behavior_loader/*.pdf
|
||||||
tests/e2e/node_modules/**
|
tests/e2e/node_modules/**
|
||||||
|
lab/case/thesis/runs*/
|
||||||
|
sim/case/thesis_simplified/runs*/
|
||||||
|
PHANTOM_web/*
|
||||||
|
|||||||
103
Makefile
103
Makefile
@@ -9,11 +9,43 @@ PYTHON := $(VENV)/bin/python
|
|||||||
PIP := $(VENV)/bin/pip
|
PIP := $(VENV)/bin/pip
|
||||||
PYTEST := $(VENV)/bin/pytest
|
PYTEST := $(VENV)/bin/pytest
|
||||||
|
|
||||||
|
SWEEP_ENV_FILE ?= .env.sweep
|
||||||
|
|
||||||
|
WANDB_ENTITY ?=
|
||||||
|
WANDB_PROJECT ?= phantom-pricing
|
||||||
|
SWEEP_ID ?=
|
||||||
|
LOCAL_TRAIN_ARGS ?= --algo ppo --total-timesteps 50000
|
||||||
|
AGENT_COUNT ?= 0
|
||||||
|
|
||||||
|
REPO_URL ?=
|
||||||
|
BRANCH ?= main
|
||||||
|
WORKDIR ?= $(HOME)/PHANTOM-agent
|
||||||
|
AGENT_LOOP ?= 1
|
||||||
|
RETRY_SECONDS ?= 20
|
||||||
|
|
||||||
|
TRAIN_IMAGE_REF := us-central1-docker.pkg.dev/phantom-trc/phantom/phantom-trainer
|
||||||
|
TPU_NAME ?=
|
||||||
|
TPU_ZONE ?= us-central2-b
|
||||||
|
|
||||||
|
SWEEP_ENV_LOAD = set -a; [ -f "$(SWEEP_ENV_FILE)" ] && . "$(SWEEP_ENV_FILE)" || true; set +a
|
||||||
|
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help:
|
help:
|
||||||
@echo "pdf.build pdf.watch pdf.clean | test.backend test.e2e test.all | web.dev | install | stats.lines"
|
@echo "pdf.build pdf.watch pdf.clean | test.backend test.e2e test.all | web.dev | install | train | train.agent | train.bootstrap | train.tpu.pod | stats.lines"
|
||||||
|
@echo "docker.train.publish"
|
||||||
|
@echo ""
|
||||||
|
@echo "Local wandb run:"
|
||||||
|
@echo " make train LOCAL_TRAIN_ARGS='--algo ppo --total-timesteps 50000'"
|
||||||
|
@echo ""
|
||||||
|
@echo "Local sweep agent from this repo:"
|
||||||
|
@echo " make train.agent SWEEP_ID=entity/project/id AGENT_COUNT=5"
|
||||||
|
@echo ""
|
||||||
|
@echo "Bootstrap private repo worker from anywhere:"
|
||||||
|
@echo " make train.bootstrap REPO_URL=https://github.com/org/repo.git BRANCH=main SWEEP_ID=entity/project/id"
|
||||||
|
@echo ""
|
||||||
|
@echo "Config source: $(SWEEP_ENV_FILE) (auto-loaded)"
|
||||||
|
|
||||||
$(BUILDDIR):
|
$(BUILDDIR):
|
||||||
mkdir -p paper/$(BUILDDIR)
|
mkdir -p paper/$(BUILDDIR)
|
||||||
@@ -22,14 +54,15 @@ $(BUILDDIR):
|
|||||||
pdf.build: $(BUILDDIR)
|
pdf.build: $(BUILDDIR)
|
||||||
@bash paper/concat_code.sh
|
@bash paper/concat_code.sh
|
||||||
@cd $(SRCDIR) && \
|
@cd $(SRCDIR) && \
|
||||||
$(LATEXMK) -pdf -jobname=$(JOBNAME) \
|
$(LATEXMK) -pdf -jobname=$(JOBNAME) -f \
|
||||||
-interaction=nonstopmode -file-line-error \
|
-interaction=nonstopmode -file-line-error \
|
||||||
|
-r ../.latexmkrc \
|
||||||
-outdir=../$(BUILDDIR) $(TEX)
|
-outdir=../$(BUILDDIR) $(TEX)
|
||||||
|
|
||||||
.PHONY: pdf.watch
|
.PHONY: pdf.watch
|
||||||
pdf.watch: $(BUILDDIR)
|
pdf.watch: $(BUILDDIR)
|
||||||
@cd $(SRCDIR) && \
|
@cd $(SRCDIR) && \
|
||||||
$(LATEXMK) -pvc -pdf -jobname=$(JOBNAME) \
|
$(LATEXMK) -pvc -pdf -jobname=$(JOBNAME) -f \
|
||||||
-interaction=nonstopmode -file-line-error \
|
-interaction=nonstopmode -file-line-error \
|
||||||
-r ../.latexmkrc \
|
-r ../.latexmkrc \
|
||||||
-outdir=../$(BUILDDIR) $(TEX)
|
-outdir=../$(BUILDDIR) $(TEX)
|
||||||
@@ -69,11 +102,75 @@ $(VENV):
|
|||||||
install: $(VENV)
|
install: $(VENV)
|
||||||
$(PIP) install -r requirements.txt
|
$(PIP) install -r requirements.txt
|
||||||
|
|
||||||
|
.PHONY: train
|
||||||
|
train: install
|
||||||
|
@$(SWEEP_ENV_LOAD); test -n "$$WANDB_API_KEY" || (echo "WANDB_API_KEY required — set it in $(SWEEP_ENV_FILE)" && exit 1)
|
||||||
|
@$(SWEEP_ENV_LOAD); WANDB_API_KEY="$$WANDB_API_KEY" WANDB_ENTITY="$(WANDB_ENTITY)" WANDB_PROJECT="$(WANDB_PROJECT)" \
|
||||||
|
$(PYTHON) -m engine.train $(LOCAL_TRAIN_ARGS)
|
||||||
|
|
||||||
|
.PHONY: train.agent
|
||||||
|
train.agent: install
|
||||||
|
@$(SWEEP_ENV_LOAD); test -n "$$WANDB_API_KEY" || (echo "WANDB_API_KEY required — set it in $(SWEEP_ENV_FILE)" && exit 1)
|
||||||
|
@test -n "$(SWEEP_ID)" || (echo "SWEEP_ID required, e.g. SWEEP_ID=entity/project/id" && exit 1)
|
||||||
|
@$(SWEEP_ENV_LOAD); WANDB_API_KEY="$$WANDB_API_KEY" WANDB_ENTITY="$(WANDB_ENTITY)" WANDB_PROJECT="$(WANDB_PROJECT)" \
|
||||||
|
$(PYTHON) -m engine.train --sweep-agent --sweep-id "$(SWEEP_ID)" \
|
||||||
|
$(if $(filter-out 0,$(AGENT_COUNT)),--count $(AGENT_COUNT),)
|
||||||
|
|
||||||
|
.PHONY: train.bootstrap
|
||||||
|
train.bootstrap:
|
||||||
|
@$(SWEEP_ENV_LOAD); test -n "$$WANDB_API_KEY" || (echo "WANDB_API_KEY required — set it in $(SWEEP_ENV_FILE)" && exit 1)
|
||||||
|
@$(SWEEP_ENV_LOAD); test -n "$$GITHUB_TOKEN" || (echo "GITHUB_TOKEN required — set it in $(SWEEP_ENV_FILE)" && exit 1)
|
||||||
|
@test -n "$(REPO_URL)" || (echo "REPO_URL required, e.g. REPO_URL=https://github.com/org/repo.git" && exit 1)
|
||||||
|
@test -n "$(SWEEP_ID)" || (echo "SWEEP_ID required, e.g. SWEEP_ID=entity/project/id" && exit 1)
|
||||||
|
@$(SWEEP_ENV_LOAD); \
|
||||||
|
WANDB_API_KEY="$$WANDB_API_KEY" \
|
||||||
|
WANDB_ENTITY="$(WANDB_ENTITY)" \
|
||||||
|
WANDB_PROJECT="$(WANDB_PROJECT)" \
|
||||||
|
GITHUB_TOKEN="$$GITHUB_TOKEN" \
|
||||||
|
REPO_URL="$(REPO_URL)" \
|
||||||
|
BRANCH="$(BRANCH)" \
|
||||||
|
WORKDIR="$(WORKDIR)" \
|
||||||
|
SWEEP_ID="$(SWEEP_ID)" \
|
||||||
|
AGENT_COUNT="$(AGENT_COUNT)" \
|
||||||
|
AGENT_LOOP="$(AGENT_LOOP)" \
|
||||||
|
RETRY_SECONDS="$(RETRY_SECONDS)" \
|
||||||
|
bash scripts/wandb_agent_bootstrap.sh
|
||||||
|
|
||||||
.PHONY: stats.lines
|
.PHONY: stats.lines
|
||||||
stats.lines:
|
stats.lines:
|
||||||
@find . \( -path '*/node_modules' -o -path '*/.venv' -o -path '*/venv' \) -prune -o \
|
@find . \( -path '*/node_modules' -o -path '*/.venv' -o -path '*/venv' \) -prune -o \
|
||||||
\( -name "*.ts" -o -name "*.py" \) -type f -print0 | xargs -0 cat | wc -l
|
\( -name "*.ts" -o -name "*.py" \) -type f -print0 | xargs -0 cat | wc -l
|
||||||
|
|
||||||
|
.PHONY: wordcount
|
||||||
|
wordcount:
|
||||||
|
@echo "Counting words in main text (excluding appendix)..."
|
||||||
|
@texcount -nosub -total -sum -1 \
|
||||||
|
$(SRCDIR)/chapters/01-intro.tex \
|
||||||
|
$(SRCDIR)/chapters/02-literature-review.tex \
|
||||||
|
$(SRCDIR)/chapters/03-methodology.tex \
|
||||||
|
$(SRCDIR)/chapters/04-results.tex \
|
||||||
|
$(SRCDIR)/chapters/05-discussion.tex \
|
||||||
|
$(SRCDIR)/chapters/06-conclusion.tex
|
||||||
|
|
||||||
|
.PHONY: docker.train.publish
|
||||||
|
docker.train.publish:
|
||||||
|
docker build -f docker/Trainer.dockerfile --target gpu -t $(TRAIN_IMAGE_REF):gpu-latest .
|
||||||
|
docker push $(TRAIN_IMAGE_REF):gpu-latest
|
||||||
|
docker build -f docker/Trainer.dockerfile --target tpu -t $(TRAIN_IMAGE_REF):tpu-latest .
|
||||||
|
docker push $(TRAIN_IMAGE_REF):tpu-latest
|
||||||
|
|
||||||
|
.PHONY: train.tpu.pod
|
||||||
|
train.tpu.pod:
|
||||||
|
@test -n "$(TPU_NAME)" || (echo "TPU_NAME required, e.g. TPU_NAME=TPUlong" && exit 1)
|
||||||
|
@test -n "$(SWEEP_ID)" || (echo "SWEEP_ID required, e.g. SWEEP_ID=entity/project/id" && exit 1)
|
||||||
|
@$(SWEEP_ENV_LOAD); test -n "$$WANDB_API_KEY" || (echo "WANDB_API_KEY required — set it in $(SWEEP_ENV_FILE)" && exit 1)
|
||||||
|
gcloud compute tpus tpu-vm scp scripts/tpu_pod_run.sh $(TPU_NAME):/tmp/tpu_pod_run.sh \
|
||||||
|
--zone=$(TPU_ZONE) --project=phantom-trc --worker=all
|
||||||
|
@$(SWEEP_ENV_LOAD); \
|
||||||
|
gcloud compute tpus tpu-vm ssh $(TPU_NAME) \
|
||||||
|
--zone=$(TPU_ZONE) --project=phantom-trc --worker=all \
|
||||||
|
--command="WANDB_API_KEY='$$WANDB_API_KEY' SWEEP_ID='$(SWEEP_ID)' AGENT_COUNT='$(AGENT_COUNT)' sh /tmp/tpu_pod_run.sh"
|
||||||
|
|
||||||
.PHONY: pdf clean watch run.webapp test count-lines all
|
.PHONY: pdf clean watch run.webapp test count-lines all
|
||||||
pdf: pdf.build
|
pdf: pdf.build
|
||||||
clean: pdf.clean
|
clean: pdf.clean
|
||||||
|
|||||||
84
README.md
84
README.md
@@ -3,10 +3,92 @@
|
|||||||
### PHANTOM
|
### PHANTOM
|
||||||
|
|
||||||
[](https://github.com/velocitatem/PHANTOM/actions/workflows/latex.yml)
|
[](https://github.com/velocitatem/PHANTOM/actions/workflows/latex.yml)
|
||||||
|
[](https://pub-d5b94a3c29fd40c6b3881946e463fdb7.r2.dev/thesis-latest.pdf)
|
||||||
[](https://sites.research.google/trc/faq/)
|
[](https://sites.research.google/trc/faq/)
|
||||||
[](https://phantom-hotel.vercel.app)
|
[](https://phantom-hotel.vercel.app)
|
||||||
[](https://phantom-airline.vercel.app)
|
[](https://phantom-airline.vercel.app)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
mindmap
|
||||||
|
PHANTOM((PHANTOM Project))
|
||||||
|
North Star
|
||||||
|
Study how automated actors change markets
|
||||||
|
Build an experimentation platform for real-world-like commerce
|
||||||
|
Two-loop learning system
|
||||||
|
Online observation loop
|
||||||
|
Offline "defense gym" loop
|
||||||
|
Core Economic Questions
|
||||||
|
Price Discovery
|
||||||
|
How prices respond to demand signals
|
||||||
|
How signal quality changes with bots/agents
|
||||||
|
Demand & Elasticity
|
||||||
|
Shifts in willingness-to-pay
|
||||||
|
Short-run vs long-run elasticity
|
||||||
|
Market Efficiency & Welfare
|
||||||
|
Consumer surplus vs producer surplus
|
||||||
|
Deadweight loss from frictions/manipulation
|
||||||
|
Price Discrimination & Segmentation
|
||||||
|
Behavioral feature-based segmentation
|
||||||
|
Fairness vs profitability tradeoffs
|
||||||
|
Information Asymmetry
|
||||||
|
Agents amplify search and arbitrage
|
||||||
|
Sellers infer more about buyers; buyers infer more about sellers
|
||||||
|
Strategic Interaction
|
||||||
|
Consumers vs firms vs agents
|
||||||
|
Feedback loops: policy ↔ behavior ↔ price
|
||||||
|
Market Power & Competition
|
||||||
|
Algorithmic pricing as competitive tool
|
||||||
|
Risks: tacit coordination / "algorithmic collusion"
|
||||||
|
Externalities
|
||||||
|
Congestion and attention costs
|
||||||
|
Spillovers: one segment’s behavior affects others’ prices
|
||||||
|
System-Level View
|
||||||
|
Participants
|
||||||
|
Humans
|
||||||
|
Agents (automated buyers/actors)
|
||||||
|
Firms (pricing decision-makers)
|
||||||
|
Platform (measurement + control layer)
|
||||||
|
Markets Simulated
|
||||||
|
Repeated transactions
|
||||||
|
Limited inventory / capacity constraints (conceptually)
|
||||||
|
Time dynamics (learning over time)
|
||||||
|
Interventions
|
||||||
|
Pricing policies
|
||||||
|
Experiment assignment / randomized exposure
|
||||||
|
Agent behavioral policies (task-driven)
|
||||||
|
Measurement & Causal Inference
|
||||||
|
What is observed
|
||||||
|
Actions (search, click, purchase intent)
|
||||||
|
Context (product attributes, time, exposure)
|
||||||
|
Outcomes (conversion, revenue, churn proxies)
|
||||||
|
Identification strategy
|
||||||
|
A/B tests and randomization
|
||||||
|
Counterfactual baselines
|
||||||
|
Robustness checks (offline replay)
|
||||||
|
Key metrics
|
||||||
|
Revenue / profit proxies
|
||||||
|
Conversion & bounce
|
||||||
|
Price volatility / stability
|
||||||
|
Welfare proxies (e.g., dispersion, access)
|
||||||
|
Risk, Governance, and Ethics
|
||||||
|
Manipulation & Integrity
|
||||||
|
Bot-driven demand distortion
|
||||||
|
Measurement contamination
|
||||||
|
Fairness & Transparency
|
||||||
|
Differential pricing concerns
|
||||||
|
Explainability and auditability
|
||||||
|
Safety Constraints
|
||||||
|
Guardrails on price moves
|
||||||
|
Monitoring for runaway feedback loops
|
||||||
|
Outputs
|
||||||
|
Insights
|
||||||
|
When do agents raise/lower prices via behavior shifts?
|
||||||
|
Which market designs are robust to automation?
|
||||||
|
Defenses
|
||||||
|
Agent-aware pricing policies (robust control)
|
||||||
|
Detection + mitigation strategies (feature-level separability)
|
||||||
|
Platform Value
|
||||||
|
Reusable testbed for market + AI-agent research
|
||||||
|
```
|
||||||
|
|||||||
6
TPUS/README.md
Normal file
6
TPUS/README.md
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
64 spot Cloud TPU v6e chips in zone europe-west4-a
|
||||||
|
32 spot Cloud TPU v4 chips in zone us-central2-b
|
||||||
|
64 spot Cloud TPU v5e chips in zone us-central1-a
|
||||||
|
64 spot Cloud TPU v6e chips in zone us-east1-d
|
||||||
|
32 on-demand Cloud TPU v4 chips in zone us-central2-b
|
||||||
|
64 spot Cloud TPU v5e chips in zone europe-west4-b
|
||||||
22
TPUS/v4_32_spot_uscentral2b.sh
Normal file
22
TPUS/v4_32_spot_uscentral2b.sh
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 32 spot Cloud TPU v4 chips in zone us-central2-b
|
||||||
|
export PROJECT_ID=phantom-trc
|
||||||
|
export QR_NAME=TPUv4s32spotUC2B
|
||||||
|
export TPU_NAME=tpu-v4-32-uc2b-spot
|
||||||
|
export ZONE=us-central2-b
|
||||||
|
export ACCELERATOR_TYPE=v4-32
|
||||||
|
export RUNTIME_VERSION=v2-alpha-tpuv4
|
||||||
|
|
||||||
|
gcloud compute tpus tpu-vm create ${TPU_NAME} \
|
||||||
|
--project=${PROJECT_ID} \
|
||||||
|
--zone=${ZONE} \
|
||||||
|
--accelerator-type=${ACCELERATOR_TYPE} \
|
||||||
|
--version=${RUNTIME_VERSION} \
|
||||||
|
--spot \
|
||||||
|
|| \
|
||||||
|
gcloud compute tpus queued-resources create ${QR_NAME} \
|
||||||
|
--project=${PROJECT_ID} \
|
||||||
|
--zone=${ZONE} \
|
||||||
|
--node-id=${TPU_NAME} \
|
||||||
|
--accelerator-type=${ACCELERATOR_TYPE} \
|
||||||
|
--runtime-version=${RUNTIME_VERSION} \
|
||||||
|
--spot
|
||||||
13
TPUS/v4_uscentral2b.sh
Normal file
13
TPUS/v4_uscentral2b.sh
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# 32 on-demand Cloud TPU v4 chips in zone us-central2-b
|
||||||
|
export PROJECT_ID=phantom-trc
|
||||||
|
export QR_NAME=TPUlong
|
||||||
|
export ZONE=us-central2-b
|
||||||
|
export ACCELERATOR_TYPE=v4-32
|
||||||
|
export RUNTIME_VERSION=v2-alpha-tpuv4
|
||||||
|
#gcloud compute tpus tpu-vm create ${TPU_NAME} --zone=${ZONE} --project=${PROJECT_ID} --accelerator-type=${ACCELERATOR_TYPE} --version=${RUNTIME_VERSION}
|
||||||
|
gcloud compute tpus queued-resources create ${QR_NAME} \
|
||||||
|
--project=${PROJECT_ID} \
|
||||||
|
--zone=${ZONE} \
|
||||||
|
--node-id=${TPU_NAME} \
|
||||||
|
--accelerator-type=${ACCELERATOR_TYPE} \
|
||||||
|
--runtime-version=${RUNTIME_VERSION}
|
||||||
22
TPUS/v5e_64_spot_europewest4b.sh
Normal file
22
TPUS/v5e_64_spot_europewest4b.sh
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 64 spot Cloud TPU v5e chips in zone europe-west4-b
|
||||||
|
export PROJECT_ID=phantom-trc
|
||||||
|
export QR_NAME=TPUv5e64spotEW4B
|
||||||
|
export TPU_NAME=tpu-v5e-64-ew4b
|
||||||
|
export ZONE=europe-west4-b
|
||||||
|
export ACCELERATOR_TYPE=v5e-64
|
||||||
|
export RUNTIME_VERSION=v2-alpha-tpuv5-lite
|
||||||
|
|
||||||
|
gcloud compute tpus tpu-vm create ${TPU_NAME} \
|
||||||
|
--project=${PROJECT_ID} \
|
||||||
|
--zone=${ZONE} \
|
||||||
|
--accelerator-type=${ACCELERATOR_TYPE} \
|
||||||
|
--version=${RUNTIME_VERSION} \
|
||||||
|
--spot \
|
||||||
|
|| \
|
||||||
|
gcloud compute tpus queued-resources create ${QR_NAME} \
|
||||||
|
--project=${PROJECT_ID} \
|
||||||
|
--zone=${ZONE} \
|
||||||
|
--node-id=${TPU_NAME} \
|
||||||
|
--accelerator-type=${ACCELERATOR_TYPE} \
|
||||||
|
--runtime-version=${RUNTIME_VERSION} \
|
||||||
|
--spot
|
||||||
22
TPUS/v5e_64_spot_uscentral1a.sh
Normal file
22
TPUS/v5e_64_spot_uscentral1a.sh
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 64 spot Cloud TPU v5e chips in zone us-central1-a
|
||||||
|
export PROJECT_ID=phantom-trc
|
||||||
|
export QR_NAME=TPUv5e64spotUC1A
|
||||||
|
export TPU_NAME=tpu-v5e-64-uc1a
|
||||||
|
export ZONE=us-central1-a
|
||||||
|
export ACCELERATOR_TYPE=v5e-64
|
||||||
|
export RUNTIME_VERSION=v2-alpha-tpuv5-lite
|
||||||
|
|
||||||
|
gcloud compute tpus tpu-vm create ${TPU_NAME} \
|
||||||
|
--project=${PROJECT_ID} \
|
||||||
|
--zone=${ZONE} \
|
||||||
|
--accelerator-type=${ACCELERATOR_TYPE} \
|
||||||
|
--version=${RUNTIME_VERSION} \
|
||||||
|
--spot \
|
||||||
|
|| \
|
||||||
|
gcloud compute tpus queued-resources create ${QR_NAME} \
|
||||||
|
--project=${PROJECT_ID} \
|
||||||
|
--zone=${ZONE} \
|
||||||
|
--node-id=${TPU_NAME} \
|
||||||
|
--accelerator-type=${ACCELERATOR_TYPE} \
|
||||||
|
--runtime-version=${RUNTIME_VERSION} \
|
||||||
|
--spot
|
||||||
22
TPUS/v6e_64_spot_europewest4a.sh
Normal file
22
TPUS/v6e_64_spot_europewest4a.sh
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 64 spot Cloud TPU v6e chips in zone europe-west4-a
|
||||||
|
export PROJECT_ID=phantom-trc
|
||||||
|
export QR_NAME=TPUv6e64spotEW4A
|
||||||
|
export TPU_NAME=tpu-v6e-64-ew4a
|
||||||
|
export ZONE=europe-west4-a
|
||||||
|
export ACCELERATOR_TYPE=v6e-64
|
||||||
|
export RUNTIME_VERSION=v2-alpha-tpuv6e
|
||||||
|
|
||||||
|
gcloud compute tpus tpu-vm create ${TPU_NAME} \
|
||||||
|
--project=${PROJECT_ID} \
|
||||||
|
--zone=${ZONE} \
|
||||||
|
--accelerator-type=${ACCELERATOR_TYPE} \
|
||||||
|
--version=${RUNTIME_VERSION} \
|
||||||
|
--spot \
|
||||||
|
|| \
|
||||||
|
gcloud compute tpus queued-resources create ${QR_NAME} \
|
||||||
|
--project=${PROJECT_ID} \
|
||||||
|
--zone=${ZONE} \
|
||||||
|
--node-id=${TPU_NAME} \
|
||||||
|
--accelerator-type=${ACCELERATOR_TYPE} \
|
||||||
|
--runtime-version=${RUNTIME_VERSION} \
|
||||||
|
--spot
|
||||||
22
TPUS/v6e_64_spot_useast1d.sh
Normal file
22
TPUS/v6e_64_spot_useast1d.sh
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# 64 spot Cloud TPU v6e chips in zone us-east1-d
|
||||||
|
export PROJECT_ID=phantom-trc
|
||||||
|
export QR_NAME=TPUv6e64spotUE1D
|
||||||
|
export TPU_NAME=tpu-v6e-64-ue1d
|
||||||
|
export ZONE=us-east1-d
|
||||||
|
export ACCELERATOR_TYPE=v6e-64
|
||||||
|
export RUNTIME_VERSION=v2-alpha-tpuv6e
|
||||||
|
|
||||||
|
gcloud compute tpus tpu-vm create ${TPU_NAME} \
|
||||||
|
--project=${PROJECT_ID} \
|
||||||
|
--zone=${ZONE} \
|
||||||
|
--accelerator-type=${ACCELERATOR_TYPE} \
|
||||||
|
--version=${RUNTIME_VERSION} \
|
||||||
|
--spot \
|
||||||
|
|| \
|
||||||
|
gcloud compute tpus queued-resources create ${QR_NAME} \
|
||||||
|
--project=${PROJECT_ID} \
|
||||||
|
--zone=${ZONE} \
|
||||||
|
--node-id=${TPU_NAME} \
|
||||||
|
--accelerator-type=${ACCELERATOR_TYPE} \
|
||||||
|
--runtime-version=${RUNTIME_VERSION} \
|
||||||
|
--spot
|
||||||
42
docker/Trainer.dockerfile
Normal file
42
docker/Trainer.dockerfile
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
FROM pytorch/pytorch:2.5.1-cuda12.4-cudnn9-runtime AS gpu
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY docker/trainer.requirements.txt /tmp/requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r /tmp/requirements.txt
|
||||||
|
|
||||||
|
# Optional for JAX-on-GPU workflows.
|
||||||
|
ARG INSTALL_JAX_GPU=false
|
||||||
|
RUN if [ "${INSTALL_JAX_GPU}" = "true" ]; then \
|
||||||
|
pip install --no-cache-dir "jax[cuda12]==0.4.30" -f https://storage.googleapis.com/jax-releases/jax_cuda_releases.html; \
|
||||||
|
fi
|
||||||
|
|
||||||
|
COPY --chmod=755 docker/trainer-agent-entrypoint.sh /usr/local/bin/trainer-agent-entrypoint
|
||||||
|
COPY engine /app/engine
|
||||||
|
|
||||||
|
ENV PYTHONPATH=/app \
|
||||||
|
XLA_PYTHON_CLIENT_PREALLOCATE=false
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/trainer-agent-entrypoint"]
|
||||||
|
|
||||||
|
|
||||||
|
FROM python:3.11-slim AS tpu
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY docker/trainer.requirements.txt /tmp/requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r /tmp/requirements.txt
|
||||||
|
|
||||||
|
RUN pip install --no-cache-dir "jax[tpu]==0.4.30" -f https://storage.googleapis.com/jax-releases/libtpu_releases.html
|
||||||
|
|
||||||
|
COPY --chmod=755 docker/trainer-agent-entrypoint.sh /usr/local/bin/trainer-agent-entrypoint
|
||||||
|
COPY engine /app/engine
|
||||||
|
|
||||||
|
ENV PYTHONPATH=/app \
|
||||||
|
PHANTOM_USE_JAX=1 \
|
||||||
|
PHANTOM_DEFAULT_AGENT_ARGS="--jax" \
|
||||||
|
XLA_PYTHON_CLIENT_PREALLOCATE=false
|
||||||
|
|
||||||
|
ENTRYPOINT ["/usr/local/bin/trainer-agent-entrypoint"]
|
||||||
23
docker/trainer-agent-entrypoint.sh
Normal file
23
docker/trainer-agent-entrypoint.sh
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
if [ -z "${SWEEP_ID:-}" ]; then
|
||||||
|
echo "SWEEP_ID is required"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -- python -m engine.train --sweep-agent --sweep-id "${SWEEP_ID}"
|
||||||
|
|
||||||
|
if [ -n "${PHANTOM_DEFAULT_AGENT_ARGS:-}" ]; then
|
||||||
|
set -- "$@" ${PHANTOM_DEFAULT_AGENT_ARGS}
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "${TRAIN_ARGS:-}" ]; then
|
||||||
|
set -- "$@" ${TRAIN_ARGS}
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "${AGENT_COUNT:-0}" != "0" ]; then
|
||||||
|
set -- "$@" --count "${AGENT_COUNT}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec "$@"
|
||||||
13
docker/trainer.requirements.txt
Normal file
13
docker/trainer.requirements.txt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
numpy>=1.24.0
|
||||||
|
pandas>=2.0.0
|
||||||
|
scipy>=1.11.0
|
||||||
|
gymnasium>=0.29.0
|
||||||
|
stable-baselines3>=2.2.0
|
||||||
|
tensorboard>=2.15.0
|
||||||
|
wandb>=0.17.0
|
||||||
|
tensorflow-probability==0.24.0
|
||||||
|
flax==0.10.7
|
||||||
|
optax==0.2.7
|
||||||
|
distrax==0.1.5
|
||||||
|
orbax-checkpoint==0.11.32
|
||||||
|
chex==0.1.90
|
||||||
21
docs/goals/goals.csv
Normal file
21
docs/goals/goals.csv
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
store_mode,task_name,task_description,definition_of_done
|
||||||
|
airline,The Indecisive Executive (SEA-LAX),"You are traveling SEA to LAX for business. You prefer Business Class for the comfort, but you need to justify the expense to your company. 1) Find the Business Class option and check its price. 2) Compare it against the Economy option on the same route to see how much money you are saving or spending. 3) Spend some time weighing the pros and cons of the ""Flexible"" fare rule vs the standard one. 4) Ultimately, decide that your comfort is worth it and book the Business Class ticket.","Booking for SEA-LAX Business Class is completed."
|
||||||
|
airline,The Cross-Country Splurge (LAX-JFK),"You are flying LAX to JFK and want to treat yourself to First Class, but only if it's the right flight. 1) Find the First Class option. 2) thoroughly check the details (duration, arrival time). 3) Compare it with the Business Class option if available, or just look at other departure times to ensure this is the best schedule. 4) After confirming this is the absolute best option, proceed to book First Class.","Booking for LAX-JFK First Class is completed."
|
||||||
|
airline,The Budget Student (DFW-ORD),"You are a broke student flying DFW to ORD. You have a budget of roughly $200. 1) Find the cheapest Economy flight. 2) Before booking, frantically check if there are any other flights or if the ""Premium"" economy is somehow cheaper (it won't be, but you should check). 3) Hesitate for a moment to consider if you should just drive instead. 4) Resign yourself to the flight and book the Economy ticket.","Booking for DFW-ORD Economy Class is completed."
|
||||||
|
airline,The Quick Hop Commuter (LAX-SFO),"You need to get from LAX to SFO as fast as possible. Price is secondary to speed. 1) Search for flights and identify the one with the shortest duration (1h 30m). 2) Click into the details to verify the arrival time fits your schedule. 3) briefly explore if there's a Business Class upgrade available for this short flight. 4) Decide to stick with Economy since it's such a short trip and book it.","Booking for LAX-SFO is completed."
|
||||||
|
airline,The Status Chaser (SFO-SEA),"You are trying to earn airline points and need a ""Premium"" class ticket specifically. 1) Search SFO to SEA. 2) Filter or look for the Premium Economy option. 3) Compare the price gap between Premium and Standard Economy. 4) Browse the details to see if the ""Premium"" fare includes better baggage allowance. 5) Conclude it's worth the points and book the Premium seat.","Booking for SFO-SEA Premium Economy is completed."
|
||||||
|
airline,The Family Reunion (MIA-ATL),"You are booking for a family of 4 (2 adults, 2 children) flying MIA to ATL. 1) Search for 4 passengers. 2) You prefer Premium, but if the total is too high, you might settle for Economy. 3) Add Premium to your cart, look at the total, and hesitate. 4) Go back and check the Economy price for 4 people. 5) Decide to treat your family and go back to book the Premium option.","Booking for MIA-ATL (Premium) is completed."
|
||||||
|
airline,The Red Eye Skeptic (LAX-JFK),"You need to fly LAX to JFK but hate late arrivals. 1) Search for the flight and check the arrival time of the First Class option. 2) It arrives early morning (02:15), which worries you. 3) Spend some time looking for other flight options on different days to see if there's a better schedule. 4) Realize this is the only direct option that works and proceed to book it despite the time.","Booking for LAX-JFK is completed."
|
||||||
|
airline,The Refundable Requirement (ATL-DFW),"Your meeting in Dallas might get cancelled, so you strictly need a ""Refundable"" ticket. 1) Search ATL to DFW. 2) Find the First Class option and verify it lists ""Refundable"". 3) Check the Economy option to see if it is also refundable (it might not be). 4) Weigh the cost difference. 5) Choose the First Class Refundable option for peace of mind.","Booking for ATL-DFW First Class is completed."
|
||||||
|
airline,The Hub Connector (ORD-MIA),"You are flying ORD to MIA to catch a cruise. You cannot be late. 1) Search for the flight. 2) Verify the ""stops"" is 0 (Direct). 3) Click into details to check the duration. 4) Worry that 3h 30m might be too long in Economy. 5) Look for a Business class option. 6) Decide to save money for the cruise and book Economy.","Booking for ORD-MIA Economy is completed."
|
||||||
|
airline,The West Coast Hopper (SEA-LAX Business),"You fly this route often and usually pay around $700. 1) Search SEA to LAX. 2) Find the Business Class ticket. 3) Check if the price is near your usual $720 or if it's surged. 4) If it looks expensive, browse other dates to compare. 5) Return to your original desired date and book the Business Class seat.","Booking for SEA-LAX Business is completed."
|
||||||
|
hotel,The Honeymoon Suite (Presidential),"It is your honeymoon. You want the best room available, specifically one with a ""jacuzzi"". 1) Search for a room for 2 people. 2) Identify the ""Presidential Suite"". 3) Click details to confirm the amenities include a jacuzzi. 4) Browse the ""Executive Suite"" just to see what you are upgrading from. 5) Go back to the Presidential Suite, confirm it's the one you want, and book it.","Booking for the Presidential Suite is completed."
|
||||||
|
hotel,The Digital Nomad (Executive),"You are working remotely and strictly need a ""workspace"". 1) Search for a room. 2) Check the ""Executive Suite"" details for a workspace. 3) Check the ""Deluxe Room"" to see if it also has a workspace and is cheaper. 4) Compare the images (if available) or amenity lists of both. 5) Decide the Executive Suite looks more comfortable for a week of work and book it.","Booking for the Executive Suite is completed."
|
||||||
|
hotel,The Safety First (Superior),"You are traveling with valuables and need a ""safe"" in the room. 1) Search for a room. 2) Look at the ""Standard Room"" amenities. Does it have a safe? 3) Look at the ""Superior Room"". Verify it has a safe. 4) Compare the price difference. Is safety worth the extra cost? 5) Decide it is, and book the Superior Room.","Booking for the Superior Room is completed."
|
||||||
|
hotel,The Bachelor Party (Max Occupancy),"You are booking for 4 guys. You want everyone in one room if possible. 1) Search for 4 adults. 2) Find the room that fits 4 people (Presidential). 3) It looks expensive. Go back and search for 2 adults to see the price of a ""Standard Room"". 4) Calculate if booking two Standard Rooms is cheaper than one Presidential. 5) Decide it's too much hassle to manage two bookings and book the Presidential Suite.","Booking for the Presidential Suite is completed."
|
||||||
|
hotel,The Budget Refundable (Junior),"You want a cheap room but your dates might change, so it MUST be refundable. 1) Search for a room. 2) Sort by price or find the cheapest options. 3) Check the ""Standard"" and ""Superior"" rooms. Notice they are likely Non-Refundable. 4) Find the ""Junior Suite"" which is Refundable. 5) Grumble about the price difference but book the Junior Suite because you need the flexibility.","Booking for the Junior Suite is completed."
|
||||||
|
hotel,The View Hunter (Executive),"You want a room with a ""city_view"" or balcony. 1) Search for a room. 2) Check the amenities of the ""Deluxe Room"". 3) Check the amenities of the ""Executive Suite"". 4) Compare the prices. 5) Decide to treat yourself to the Executive Suite for the better view/balcony and book it.","Booking for the Executive Suite is completed."
|
||||||
|
hotel,The Just-A-Bed (Standard),"You just need a place to crash. Lowest price wins. 1) Search for a room. 2) Identify the absolute cheapest option (Standard Room). 3) Click details just to make sure it has ""wifi"". 4) Briefly glance at the ""Superior Room"" to see if the upgrade is <$10. 5) If not, go back and book the Standard Room immediately.","Booking for the Standard Room is completed."
|
||||||
|
hotel,The Family Vacation (Deluxe),"You are traveling with a child. You need a room that isn't too cramped but not a suite. 1) Search for 2 adults, 1 child. 2) Look at the ""Deluxe Room"". 3) Check the amenities for ""coffee_maker"" (parents need coffee). 4) Compare it with the ""Junior Suite"". 5) Decide the Deluxe Room is sufficient value and book it.","Booking for the Deluxe Room is completed."
|
||||||
|
hotel,The Long Stay (Junior),"You are staying for 7 nights. You want something nicer than a standard room but affordable. 1) Search for a room. 2) Look at the ""Junior Suite"". 3) Check the amenities for a ""mini_fridge"" or similar. 4) Compare the total cost for 7 nights against your budget. 5) Hesitate and look at the ""Standard Room"" price. 6) Decide the extra space of the Junior Suite is worth it for a long stay and book it.","Booking for the Junior Suite is completed."
|
||||||
|
hotel,The Last Minute Panic (Superior),"It's late and you need a room for tonight. 1) Search for a room for 1 person. 2) You recognize the ""Superior Room"" brand. 3) Click it. 4) Quickly verify check-in times or details. 5) Don't overthink it—book the Superior Room as fast as possible.","Booking for the Superior Room is completed."
|
||||||
|
@@ -47,7 +47,7 @@
|
|||||||
<meta name="citation_author" content="Rösel, Daniel">
|
<meta name="citation_author" content="Rösel, Daniel">
|
||||||
<meta name="citation_publication_date" content="2025">
|
<meta name="citation_publication_date" content="2025">
|
||||||
<meta name="citation_conference_title" content="IE University Bachelor's Thesis">
|
<meta name="citation_conference_title" content="IE University Bachelor's Thesis">
|
||||||
<meta name="citation_pdf_url" content="TODO">
|
<meta name="citation_pdf_url" content="https://pub-d5b94a3c29fd40c6b3881946e463fdb7.r2.dev/thesis-latest.pdf">
|
||||||
|
|
||||||
<!-- Additional SEO -->
|
<!-- Additional SEO -->
|
||||||
<meta name="theme-color" content="#2563eb">
|
<meta name="theme-color" content="#2563eb">
|
||||||
@@ -233,14 +233,13 @@
|
|||||||
|
|
||||||
<div class="is-size-5 publication-authors">
|
<div class="is-size-5 publication-authors">
|
||||||
<span class="author-block">IE University<br>Bachelor's Thesis 2025</span>
|
<span class="author-block">IE University<br>Bachelor's Thesis 2025</span>
|
||||||
<span class="eql-cntrb"><small><br>Advisor: <a href="SECOND AUTHOR PERSONAL LINK" target="_blank">Alberto Martín Izquierdo</a></small></span>
|
<span class="eql-cntrb"><small><br>Advisor: Alberto Martín Izquierdo</small></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="column has-text-centered">
|
<div class="column has-text-centered">
|
||||||
<div class="publication-links">
|
<div class="publication-links">
|
||||||
<!-- TODO: Update with your arXiv paper ID -->
|
|
||||||
<span class="link-block">
|
<span class="link-block">
|
||||||
<a href="https://arxiv.org/pdf/<ARXIV PAPER ID>.pdf" target="_blank"
|
<a href="https://pub-d5b94a3c29fd40c6b3881946e463fdb7.r2.dev/thesis-latest.pdf" target="_blank"
|
||||||
class="external-link button is-normal is-rounded is-dark">
|
class="external-link button is-normal is-rounded is-dark">
|
||||||
<span class="icon">
|
<span class="icon">
|
||||||
<i class="fas fa-file-pdf"></i>
|
<i class="fas fa-file-pdf"></i>
|
||||||
@@ -315,7 +314,10 @@
|
|||||||
<h2 class="title is-3">Abstract</h2>
|
<h2 class="title is-3">Abstract</h2>
|
||||||
<div class="content has-text-justified">
|
<div class="content has-text-justified">
|
||||||
<p>
|
<p>
|
||||||
The primary objective of this thesis is to develop and validate pricing heuristics that protect e-commerce platforms from systematic exploitation by Large Language Model (LLM) agents within dynamic pricing environments. As AI agents increasingly mediate consumer transactions, they enable users to circumvent the Cost of Information (the price premium accumulated through demand signal expression) by conducting reconnaissance in isolated sessions before executing purchases through clean sessions at base prices. This research will make an anticipatory contribution by adapting recommendation system methodologies to distinguish between genuine human browsing behaviour and agent-orchestrated information gathering, thereby enabling pricing systems to maintain margin integrity without degrading the user experience for legitimate customers or getting rid of leads generated by LLMs.
|
This research establishes the following contributions: definition and formalization of non-human transactors in e-commerce platforms, development of a testing-ground for capturing the behavioral essence of these transactors across a large variety of digital systems, construction of a discriminative model to prove separability as a strong learner for downstream mitigation of contamination by non-human entities, translation of such learned separability into existing dynamic pricing machine learning loops, and establishment of a high-level KPI-affecting causal effect and cost-saving framework for the future of internet commerce in the presence of such non-human learners.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
This work develops behavioral signature models using recommendation system techniques to profile session-level interaction, temporal engagement, and cross-session correlation. The AI Agent market is forecasted to grow from around USD 5-8 billion in 2025 to USD 42-52 billion by 2030, raising the question of how these systems should be designed for future robustness and how to maintain a competitive edge in the analytical components of e-commerce platforms.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -433,8 +435,7 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 class="title">Poster</h2>
|
<h2 class="title">Poster</h2>
|
||||||
|
|
||||||
<!-- TODO: Replace with your poster PDF -->
|
<iframe src="https://pub-d5b94a3c29fd40c6b3881946e463fdb7.r2.dev/thesis-latest.pdf" width="100%" height="550">
|
||||||
<iframe src="static/pdfs/sample.pdf" width="100%" height="550">
|
|
||||||
</iframe>
|
</iframe>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
97
engine/engine.py
Normal file
97
engine/engine.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
from sys import platform
|
||||||
|
import numpy as np
|
||||||
|
from .lib.demand import generate_demand_for_actor, estimate_demand
|
||||||
|
from .lib.behavior import sample_behavior
|
||||||
|
from logging import INFO, getLogger
|
||||||
|
|
||||||
|
logger = getLogger(__name__)
|
||||||
|
logger.setLevel(INFO)
|
||||||
|
|
||||||
|
|
||||||
|
class MarketEngine:
|
||||||
|
"""implements separate demand distributions for humans and agents per Section 3.1.1"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
alpha: float,
|
||||||
|
N: int,
|
||||||
|
human_params: tuple,
|
||||||
|
agent_params: tuple,
|
||||||
|
demand_distribution=np.random.normal,
|
||||||
|
noise_std: float = 1.0,
|
||||||
|
action_weights: dict | None = None,
|
||||||
|
):
|
||||||
|
# no defaults for D_H, D_A - force explicit experiment design
|
||||||
|
self.alpha = alpha
|
||||||
|
self.N = int(N)
|
||||||
|
self.Nagents = int(N * alpha)
|
||||||
|
self.Nhumans = int(N * (1 - alpha))
|
||||||
|
self.human_params = human_params
|
||||||
|
self.agent_params = agent_params
|
||||||
|
self.noise_std = noise_std
|
||||||
|
self.demand_dist = demand_distribution
|
||||||
|
self.action_weights = action_weights
|
||||||
|
|
||||||
|
def act(self, prices):
|
||||||
|
# generate separate demands d() per actor type
|
||||||
|
demand_h = generate_demand_for_actor(
|
||||||
|
prices,
|
||||||
|
self.human_params,
|
||||||
|
self.noise_std,
|
||||||
|
distribution_method=self.demand_dist,
|
||||||
|
)
|
||||||
|
demand_a = generate_demand_for_actor(
|
||||||
|
prices,
|
||||||
|
self.agent_params,
|
||||||
|
self.noise_std,
|
||||||
|
distribution_method=self.demand_dist,
|
||||||
|
)
|
||||||
|
# sample behavior trajectories from each demand distribution
|
||||||
|
human_t = [sample_behavior(demand_h, human=True) for _ in range(self.Nhumans)]
|
||||||
|
agent_t = [sample_behavior(demand_a, human=False) for _ in range(self.Nagents)]
|
||||||
|
# store trajectories for agent probability calculation
|
||||||
|
self.last_trajectories = human_t + agent_t
|
||||||
|
return estimate_demand(self.last_trajectories, self.action_weights)
|
||||||
|
|
||||||
|
def measure(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PricingEngine:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def act(self, demand):
|
||||||
|
return np.random.uniform(low=25, high=100, size=10)
|
||||||
|
|
||||||
|
|
||||||
|
class Limbo:
|
||||||
|
def __init__(self, platform, market) -> None:
|
||||||
|
self.platform_turn = True
|
||||||
|
self.platform = platform
|
||||||
|
self.market = market
|
||||||
|
self.output = None
|
||||||
|
|
||||||
|
def step(self):
|
||||||
|
if self.platform_turn:
|
||||||
|
self.output = self.platform.act(self.output)
|
||||||
|
else:
|
||||||
|
self.output = self.market.act(self.output)
|
||||||
|
self.platform_turn = not self.platform_turn
|
||||||
|
return self.output
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
self.platform_turn = True
|
||||||
|
self.output = None
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
platform = PricingEngine()
|
||||||
|
market = MarketEngine(
|
||||||
|
alpha=0.3, N=100, human_params=(50, 10), agent_params=(45, 15)
|
||||||
|
)
|
||||||
|
limbo = Limbo(platform, market)
|
||||||
|
for _ in range(10):
|
||||||
|
limbo.step()
|
||||||
13
engine/jax/__init__.py
Normal file
13
engine/jax/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
"""JAX-compatible training and environment modules for PHANTOM."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jax # noqa: F401
|
||||||
|
import jax.numpy as jnp # noqa: F401
|
||||||
|
|
||||||
|
JAX_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
JAX_AVAILABLE = False
|
||||||
|
|
||||||
|
__all__ = ["JAX_AVAILABLE"]
|
||||||
49
engine/jax/checkpoint.py
Normal file
49
engine/jax/checkpoint.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
"""Orbax checkpoint helpers for JAX training runs."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
import orbax.checkpoint as ocp
|
||||||
|
|
||||||
|
HAS_ORBAX = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_ORBAX = False
|
||||||
|
|
||||||
|
|
||||||
|
def _require_orbax() -> None:
|
||||||
|
if not HAS_ORBAX:
|
||||||
|
raise ImportError(
|
||||||
|
"orbax-checkpoint is required for checkpoint support. "
|
||||||
|
"Install engine/jax/requirements.txt first."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_manager(directory: str | Path, max_to_keep: int = 5):
|
||||||
|
_require_orbax()
|
||||||
|
root = Path(directory)
|
||||||
|
root.mkdir(parents=True, exist_ok=True)
|
||||||
|
options = ocp.CheckpointManagerOptions(
|
||||||
|
max_to_keep=max(1, int(max_to_keep)), create=True
|
||||||
|
)
|
||||||
|
return ocp.CheckpointManager(root.as_posix(), ocp.PyTreeCheckpointer(), options)
|
||||||
|
|
||||||
|
|
||||||
|
def save(manager, *, step: int, payload: Any) -> bool:
|
||||||
|
_require_orbax()
|
||||||
|
return bool(manager.save(int(step), payload))
|
||||||
|
|
||||||
|
|
||||||
|
def latest_step(manager) -> int | None:
|
||||||
|
_require_orbax()
|
||||||
|
return manager.latest_step()
|
||||||
|
|
||||||
|
|
||||||
|
def restore(manager, *, target: Any, step: int | None = None) -> Any:
|
||||||
|
_require_orbax()
|
||||||
|
step_to_restore = manager.latest_step() if step is None else int(step)
|
||||||
|
if step_to_restore is None:
|
||||||
|
return target
|
||||||
|
return manager.restore(step_to_restore, items=target)
|
||||||
287
engine/jax/env.py
Normal file
287
engine/jax/env.py
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
"""JAX-native PHANTOM environment with robust contamination step."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jax
|
||||||
|
import jax.numpy as jnp
|
||||||
|
except ImportError as exc: # pragma: no cover
|
||||||
|
raise ImportError("engine.jax.env requires JAX") from exc
|
||||||
|
|
||||||
|
from .primitives import (
|
||||||
|
_sample_sessions_jax,
|
||||||
|
agent_probability_from_kl,
|
||||||
|
batch_kl,
|
||||||
|
compute_session_transitions,
|
||||||
|
load_transition_data,
|
||||||
|
purchase_flags,
|
||||||
|
reward_with_coi_penalty,
|
||||||
|
revenue_from_demand,
|
||||||
|
weighted_demand,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EnvParams(NamedTuple):
|
||||||
|
n_products: int
|
||||||
|
n_sessions: int
|
||||||
|
max_episode_steps: int
|
||||||
|
max_session_steps: int
|
||||||
|
price_low: float
|
||||||
|
price_high: float
|
||||||
|
lambda_coi: float
|
||||||
|
info_value: float
|
||||||
|
robust_radius: float
|
||||||
|
margin_floor: float
|
||||||
|
margin_floor_patience: int
|
||||||
|
action_scales: jax.Array
|
||||||
|
alpha_nominal: float
|
||||||
|
alpha_candidates: jax.Array
|
||||||
|
human_T: jax.Array
|
||||||
|
agent_T: jax.Array
|
||||||
|
terminal_mask: jax.Array
|
||||||
|
purchase_mask: jax.Array
|
||||||
|
event_weights: jax.Array
|
||||||
|
start_idx: int
|
||||||
|
term_idx: int
|
||||||
|
|
||||||
|
|
||||||
|
class EnvState(NamedTuple):
|
||||||
|
prices: jax.Array
|
||||||
|
demand: jax.Array
|
||||||
|
step_count: jax.Array
|
||||||
|
low_margin_streak: jax.Array
|
||||||
|
last_agent_prob: jax.Array
|
||||||
|
last_alpha_adv: jax.Array
|
||||||
|
|
||||||
|
|
||||||
|
class CandidateEval(NamedTuple):
|
||||||
|
reward: jax.Array
|
||||||
|
revenue: jax.Array
|
||||||
|
demand: jax.Array
|
||||||
|
agent_prob: jax.Array
|
||||||
|
leakage: jax.Array
|
||||||
|
discount: jax.Array
|
||||||
|
n_purchases: jax.Array
|
||||||
|
n_agents: jax.Array
|
||||||
|
|
||||||
|
|
||||||
|
def make_env_params(
|
||||||
|
*,
|
||||||
|
n_products: int,
|
||||||
|
alpha: float,
|
||||||
|
n_sessions: int,
|
||||||
|
lambda_coi: float,
|
||||||
|
robust_radius: float,
|
||||||
|
robust_points: int,
|
||||||
|
info_value: float,
|
||||||
|
action_levels: int,
|
||||||
|
action_scale_low: float,
|
||||||
|
action_scale_high: float,
|
||||||
|
price_low: float,
|
||||||
|
price_high: float,
|
||||||
|
max_episode_steps: int,
|
||||||
|
max_session_steps: int = 40,
|
||||||
|
margin_floor: float = 0.05,
|
||||||
|
margin_floor_patience: int = 5,
|
||||||
|
prefer_behavior_data: bool = True,
|
||||||
|
) -> EnvParams:
|
||||||
|
transition = load_transition_data(prefer_data=prefer_behavior_data).to_jax()
|
||||||
|
if robust_radius <= 0.0 or robust_points <= 1:
|
||||||
|
alpha_candidates = jnp.asarray([float(alpha)], dtype=jnp.float32)
|
||||||
|
else:
|
||||||
|
lo = max(0.0, float(alpha) - float(robust_radius))
|
||||||
|
hi = min(1.0, float(alpha) + float(robust_radius))
|
||||||
|
alpha_candidates = jnp.linspace(lo, hi, int(robust_points), dtype=jnp.float32)
|
||||||
|
|
||||||
|
action_scales = jnp.linspace(
|
||||||
|
float(action_scale_low),
|
||||||
|
float(action_scale_high),
|
||||||
|
int(action_levels),
|
||||||
|
dtype=jnp.float32,
|
||||||
|
)
|
||||||
|
return EnvParams(
|
||||||
|
n_products=int(n_products),
|
||||||
|
n_sessions=int(n_sessions),
|
||||||
|
max_episode_steps=int(max_episode_steps),
|
||||||
|
max_session_steps=int(max_session_steps),
|
||||||
|
price_low=float(price_low),
|
||||||
|
price_high=float(price_high),
|
||||||
|
lambda_coi=float(lambda_coi),
|
||||||
|
info_value=float(info_value),
|
||||||
|
robust_radius=float(robust_radius),
|
||||||
|
margin_floor=float(margin_floor),
|
||||||
|
margin_floor_patience=int(margin_floor_patience),
|
||||||
|
action_scales=action_scales,
|
||||||
|
alpha_nominal=float(alpha),
|
||||||
|
alpha_candidates=alpha_candidates,
|
||||||
|
human_T=jnp.asarray(transition.human_T),
|
||||||
|
agent_T=jnp.asarray(transition.agent_T),
|
||||||
|
terminal_mask=jnp.asarray(transition.terminal_mask),
|
||||||
|
purchase_mask=jnp.asarray(transition.purchase_mask),
|
||||||
|
event_weights=jnp.asarray(transition.event_weights),
|
||||||
|
start_idx=int(transition.start_idx),
|
||||||
|
term_idx=int(transition.term_idx),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _flatten_obs(demand: jax.Array, prices: jax.Array) -> jax.Array:
|
||||||
|
return jnp.concatenate([demand.astype(jnp.float32), prices.astype(jnp.float32)])
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_action(
|
||||||
|
prices: jax.Array, action: jax.Array, params: EnvParams
|
||||||
|
) -> jax.Array:
|
||||||
|
idx = jnp.clip(action.astype(jnp.int32), 0, params.action_scales.shape[0] - 1)
|
||||||
|
scale = params.action_scales[idx]
|
||||||
|
next_prices = prices * scale
|
||||||
|
return jnp.clip(next_prices, params.price_low, params.price_high)
|
||||||
|
|
||||||
|
|
||||||
|
def _evaluate_candidate(
|
||||||
|
key: jax.Array,
|
||||||
|
alpha_candidate: jax.Array,
|
||||||
|
prices: jax.Array,
|
||||||
|
params: EnvParams,
|
||||||
|
) -> CandidateEval:
|
||||||
|
states, products, actors, lengths = _sample_sessions_jax(
|
||||||
|
key,
|
||||||
|
params.human_T,
|
||||||
|
params.agent_T,
|
||||||
|
params.terminal_mask,
|
||||||
|
params.start_idx,
|
||||||
|
params.term_idx,
|
||||||
|
alpha_candidate,
|
||||||
|
params.n_products,
|
||||||
|
params.n_sessions,
|
||||||
|
params.max_session_steps,
|
||||||
|
int(params.human_T.shape[0]),
|
||||||
|
)
|
||||||
|
session_trans = compute_session_transitions(
|
||||||
|
states, lengths, int(params.human_T.shape[0])
|
||||||
|
)
|
||||||
|
delta_h, delta_a = batch_kl(session_trans, params.human_T, params.agent_T)
|
||||||
|
agent_probs = agent_probability_from_kl(delta_h, delta_a)
|
||||||
|
agent_prob = jnp.mean(agent_probs)
|
||||||
|
|
||||||
|
demand = weighted_demand(states, products, params.n_products, params.event_weights)
|
||||||
|
revenue = revenue_from_demand(prices, demand)
|
||||||
|
reward, leakage, discount = reward_with_coi_penalty(
|
||||||
|
revenue,
|
||||||
|
agent_prob,
|
||||||
|
params.lambda_coi,
|
||||||
|
params.info_value,
|
||||||
|
)
|
||||||
|
purchases = purchase_flags(states, params.purchase_mask)
|
||||||
|
return CandidateEval(
|
||||||
|
reward=reward,
|
||||||
|
revenue=revenue,
|
||||||
|
demand=demand,
|
||||||
|
agent_prob=agent_prob,
|
||||||
|
leakage=leakage,
|
||||||
|
discount=discount,
|
||||||
|
n_purchases=jnp.sum(purchases.astype(jnp.float32)),
|
||||||
|
n_agents=jnp.sum(actors.astype(jnp.float32)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def reset_env(key: jax.Array, params: EnvParams) -> tuple[jax.Array, EnvState]:
|
||||||
|
prices = jax.random.uniform(
|
||||||
|
key,
|
||||||
|
shape=(params.n_products,),
|
||||||
|
minval=params.price_low,
|
||||||
|
maxval=params.price_high,
|
||||||
|
)
|
||||||
|
demand = jnp.zeros((params.n_products,), dtype=jnp.float32)
|
||||||
|
state = EnvState(
|
||||||
|
prices=prices,
|
||||||
|
demand=demand,
|
||||||
|
step_count=jnp.asarray(0, dtype=jnp.int32),
|
||||||
|
low_margin_streak=jnp.asarray(0, dtype=jnp.int32),
|
||||||
|
last_agent_prob=jnp.asarray(params.alpha_nominal, dtype=jnp.float32),
|
||||||
|
last_alpha_adv=jnp.asarray(params.alpha_nominal, dtype=jnp.float32),
|
||||||
|
)
|
||||||
|
return _flatten_obs(demand, prices), state
|
||||||
|
|
||||||
|
|
||||||
|
def step_env(
|
||||||
|
key: jax.Array,
|
||||||
|
state: EnvState,
|
||||||
|
action: jax.Array,
|
||||||
|
params: EnvParams,
|
||||||
|
) -> tuple[jax.Array, EnvState, jax.Array, jax.Array, dict[str, jax.Array]]:
|
||||||
|
prices = _decode_action(state.prices, action, params)
|
||||||
|
n_candidates = params.alpha_candidates.shape[0]
|
||||||
|
cand_keys = jax.random.split(key, n_candidates)
|
||||||
|
evals = jax.vmap(
|
||||||
|
lambda k, a: _evaluate_candidate(k, a, prices, params),
|
||||||
|
in_axes=(0, 0),
|
||||||
|
)(cand_keys, params.alpha_candidates)
|
||||||
|
idx = jnp.argmin(evals.reward)
|
||||||
|
|
||||||
|
demand = evals.demand[idx]
|
||||||
|
reward = evals.reward[idx]
|
||||||
|
revenue = evals.revenue[idx]
|
||||||
|
agent_prob = evals.agent_prob[idx]
|
||||||
|
leakage = evals.leakage[idx]
|
||||||
|
discount = evals.discount[idx]
|
||||||
|
n_purchases = evals.n_purchases[idx]
|
||||||
|
n_agents = evals.n_agents[idx]
|
||||||
|
alpha_adv = params.alpha_candidates[idx]
|
||||||
|
|
||||||
|
step_count = state.step_count + 1
|
||||||
|
avg_price = jnp.maximum(jnp.mean(prices), 1e-6)
|
||||||
|
avg_margin = (avg_price - params.price_low) / avg_price
|
||||||
|
next_streak = jnp.where(
|
||||||
|
avg_margin < params.margin_floor, state.low_margin_streak + 1, 0
|
||||||
|
)
|
||||||
|
|
||||||
|
margin_collapsed = next_streak >= params.margin_floor_patience
|
||||||
|
done = (step_count >= params.max_episode_steps) | margin_collapsed
|
||||||
|
|
||||||
|
next_state = EnvState(
|
||||||
|
prices=prices,
|
||||||
|
demand=demand,
|
||||||
|
step_count=step_count,
|
||||||
|
low_margin_streak=next_streak,
|
||||||
|
last_agent_prob=agent_prob,
|
||||||
|
last_alpha_adv=alpha_adv,
|
||||||
|
)
|
||||||
|
obs = _flatten_obs(demand, prices)
|
||||||
|
info = {
|
||||||
|
"revenue": revenue,
|
||||||
|
"agent_prob": agent_prob,
|
||||||
|
"alpha_adv": alpha_adv,
|
||||||
|
"coi_leakage": leakage,
|
||||||
|
"coi_discount": discount,
|
||||||
|
"n_purchases": n_purchases,
|
||||||
|
"n_agents": n_agents,
|
||||||
|
"avg_margin": avg_margin,
|
||||||
|
}
|
||||||
|
return obs, next_state, reward, done, info
|
||||||
|
|
||||||
|
|
||||||
|
class PHANTOMJAXEnv:
|
||||||
|
def __init__(self, params: EnvParams):
|
||||||
|
self.params = params
|
||||||
|
|
||||||
|
def reset(self, key: jax.Array, params: EnvParams | None = None):
|
||||||
|
return reset_env(key, self.params if params is None else params)
|
||||||
|
|
||||||
|
def step(
|
||||||
|
self,
|
||||||
|
key: jax.Array,
|
||||||
|
state: EnvState,
|
||||||
|
action: jax.Array,
|
||||||
|
params: EnvParams | None = None,
|
||||||
|
):
|
||||||
|
return step_env(key, state, action, self.params if params is None else params)
|
||||||
|
|
||||||
|
def action_space_n(self, params: EnvParams | None = None) -> int:
|
||||||
|
p = self.params if params is None else params
|
||||||
|
return int(p.action_scales.shape[0])
|
||||||
|
|
||||||
|
def observation_dim(self, params: EnvParams | None = None) -> int:
|
||||||
|
p = self.params if params is None else params
|
||||||
|
return int(p.n_products * 2)
|
||||||
495
engine/jax/primitives.py
Normal file
495
engine/jax/primitives.py
Normal file
@@ -0,0 +1,495 @@
|
|||||||
|
"""JAX-compatible primitives for PHANTOM session simulation and separability."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from functools import partial
|
||||||
|
from typing import Mapping, Sequence
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
try:
|
||||||
|
import jax
|
||||||
|
import jax.numpy as jnp
|
||||||
|
|
||||||
|
JAX_AVAILABLE = True
|
||||||
|
except ImportError:
|
||||||
|
jax = None # type: ignore[assignment]
|
||||||
|
jnp = np # type: ignore[assignment]
|
||||||
|
JAX_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
STATE_START_KEYS = ("session_start", "start")
|
||||||
|
TERMINAL_EVENT_TOKENS = (
|
||||||
|
"session_end",
|
||||||
|
"end",
|
||||||
|
"purchase_complete",
|
||||||
|
"checkout_start",
|
||||||
|
"checkout",
|
||||||
|
)
|
||||||
|
PURCHASE_EVENT_TOKENS = (
|
||||||
|
"purchase_complete",
|
||||||
|
"purchase",
|
||||||
|
"checkout_start",
|
||||||
|
"checkout",
|
||||||
|
)
|
||||||
|
|
||||||
|
CATEGORY_WEIGHTS = {"cart": 4.0, "dwell": 2.0, "nav": 1.0, "filter": 0.5}
|
||||||
|
ACTION_CATEGORIES = {
|
||||||
|
"cart": {"add_item", "add_to_cart", "remove", "checkout", "purchase"},
|
||||||
|
"dwell": {
|
||||||
|
"hover_title",
|
||||||
|
"hover_paragraph",
|
||||||
|
"hover_link",
|
||||||
|
"hover_over_title",
|
||||||
|
"hover_over_paragraph",
|
||||||
|
"hover_over_link",
|
||||||
|
"hover_over_button",
|
||||||
|
},
|
||||||
|
"nav": {
|
||||||
|
"page_view",
|
||||||
|
"view_item",
|
||||||
|
"view",
|
||||||
|
"learn_more",
|
||||||
|
"learn_more_about_item",
|
||||||
|
"view_item_page",
|
||||||
|
"session_start",
|
||||||
|
},
|
||||||
|
"filter": {
|
||||||
|
"search",
|
||||||
|
"filter_date",
|
||||||
|
"filter_price",
|
||||||
|
"sort",
|
||||||
|
"filter_for_date",
|
||||||
|
"filter_for_price",
|
||||||
|
"filter_for_amenities",
|
||||||
|
"sort_change",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
DEFAULT_ACTION_WEIGHTS = {
|
||||||
|
action: CATEGORY_WEIGHTS[group]
|
||||||
|
for group, actions in ACTION_CATEGORIES.items()
|
||||||
|
for action in actions
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class TransitionData:
|
||||||
|
"""Dense transition kernels and per-state metadata."""
|
||||||
|
|
||||||
|
human_T: np.ndarray
|
||||||
|
agent_T: np.ndarray
|
||||||
|
terminal_mask: np.ndarray
|
||||||
|
purchase_mask: np.ndarray
|
||||||
|
event_weights: np.ndarray
|
||||||
|
event_names: tuple[str, ...]
|
||||||
|
start_idx: int
|
||||||
|
term_idx: int
|
||||||
|
|
||||||
|
def to_jax(self) -> "TransitionData":
|
||||||
|
if not JAX_AVAILABLE:
|
||||||
|
return self
|
||||||
|
return TransitionData(
|
||||||
|
human_T=jnp.asarray(self.human_T),
|
||||||
|
agent_T=jnp.asarray(self.agent_T),
|
||||||
|
terminal_mask=jnp.asarray(self.terminal_mask),
|
||||||
|
purchase_mask=jnp.asarray(self.purchase_mask),
|
||||||
|
event_weights=jnp.asarray(self.event_weights),
|
||||||
|
event_names=self.event_names,
|
||||||
|
start_idx=int(self.start_idx),
|
||||||
|
term_idx=int(self.term_idx),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class SessionBatch:
|
||||||
|
states: np.ndarray
|
||||||
|
products: np.ndarray
|
||||||
|
actors: np.ndarray
|
||||||
|
lengths: np.ndarray
|
||||||
|
|
||||||
|
|
||||||
|
def _event_weight(name: str) -> float:
|
||||||
|
if name in DEFAULT_ACTION_WEIGHTS:
|
||||||
|
return float(DEFAULT_ACTION_WEIGHTS[name])
|
||||||
|
if name.startswith("hover"):
|
||||||
|
return float(CATEGORY_WEIGHTS["dwell"])
|
||||||
|
if name.startswith("filter") or name in {"search", "sort", "sort_change"}:
|
||||||
|
return float(CATEGORY_WEIGHTS["filter"])
|
||||||
|
if name.startswith("add") or name in {
|
||||||
|
"checkout",
|
||||||
|
"checkout_start",
|
||||||
|
"purchase",
|
||||||
|
"remove_item",
|
||||||
|
"purchase_complete",
|
||||||
|
}:
|
||||||
|
return float(CATEGORY_WEIGHTS["cart"])
|
||||||
|
if any(token in name for token in TERMINAL_EVENT_TOKENS):
|
||||||
|
return 0.0
|
||||||
|
return float(CATEGORY_WEIGHTS["nav"])
|
||||||
|
|
||||||
|
|
||||||
|
def _is_terminal(name: str) -> bool:
|
||||||
|
return any(token in name for token in TERMINAL_EVENT_TOKENS)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_purchase(name: str) -> bool:
|
||||||
|
return any(token in name for token in PURCHASE_EVENT_TOKENS)
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_events(*transitions: Mapping[str, Mapping[str, float]]) -> tuple[str, ...]:
|
||||||
|
names: set[str] = set()
|
||||||
|
for trans in transitions:
|
||||||
|
for src, dsts in trans.items():
|
||||||
|
names.add(src)
|
||||||
|
names.update(dsts.keys())
|
||||||
|
names.discard("__terminal__")
|
||||||
|
return tuple(sorted(names))
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_rows(matrix: np.ndarray, term_idx: int) -> np.ndarray:
|
||||||
|
row_sums = matrix.sum(axis=1, keepdims=True)
|
||||||
|
dead_rows = np.isclose(row_sums.squeeze(-1), 0.0)
|
||||||
|
if np.any(dead_rows):
|
||||||
|
matrix[dead_rows] = 0.0
|
||||||
|
matrix[dead_rows, term_idx] = 1.0
|
||||||
|
row_sums = matrix.sum(axis=1, keepdims=True)
|
||||||
|
return matrix / np.maximum(row_sums, 1e-8)
|
||||||
|
|
||||||
|
|
||||||
|
def _dense_from_dict(
|
||||||
|
transitions: Mapping[str, Mapping[str, float]],
|
||||||
|
event_to_idx: Mapping[str, int],
|
||||||
|
term_idx: int,
|
||||||
|
) -> np.ndarray:
|
||||||
|
n_states = len(event_to_idx)
|
||||||
|
matrix = np.zeros((n_states, n_states), dtype=np.float32)
|
||||||
|
for src, dsts in transitions.items():
|
||||||
|
i = event_to_idx.get(src)
|
||||||
|
if i is None:
|
||||||
|
continue
|
||||||
|
for dst, prob in dsts.items():
|
||||||
|
j = event_to_idx.get(dst)
|
||||||
|
if j is None:
|
||||||
|
continue
|
||||||
|
matrix[i, j] += float(prob)
|
||||||
|
return _normalize_rows(matrix, term_idx)
|
||||||
|
|
||||||
|
|
||||||
|
def compile_transition_data(
|
||||||
|
human_transitions: Mapping[str, Mapping[str, float]],
|
||||||
|
agent_transitions: Mapping[str, Mapping[str, float]],
|
||||||
|
) -> TransitionData:
|
||||||
|
event_names = _collect_events(human_transitions, agent_transitions)
|
||||||
|
if not event_names:
|
||||||
|
return fallback_transition_data()
|
||||||
|
|
||||||
|
event_names = tuple([*event_names, "__terminal__"])
|
||||||
|
term_idx = len(event_names) - 1
|
||||||
|
event_to_idx = {name: i for i, name in enumerate(event_names)}
|
||||||
|
|
||||||
|
human_T = _dense_from_dict(human_transitions, event_to_idx, term_idx)
|
||||||
|
agent_T = _dense_from_dict(agent_transitions, event_to_idx, term_idx)
|
||||||
|
|
||||||
|
terminal_mask = np.array([_is_terminal(name) for name in event_names], dtype=bool)
|
||||||
|
purchase_mask = np.array([_is_purchase(name) for name in event_names], dtype=bool)
|
||||||
|
event_weights = np.array(
|
||||||
|
[_event_weight(name) for name in event_names], dtype=np.float32
|
||||||
|
)
|
||||||
|
|
||||||
|
terminal_mask[term_idx] = True
|
||||||
|
|
||||||
|
for idx, is_term in enumerate(terminal_mask):
|
||||||
|
if not is_term:
|
||||||
|
continue
|
||||||
|
human_T[idx] = 0.0
|
||||||
|
agent_T[idx] = 0.0
|
||||||
|
human_T[idx, idx] = 1.0
|
||||||
|
agent_T[idx, idx] = 1.0
|
||||||
|
|
||||||
|
start_idx = 0
|
||||||
|
for key in STATE_START_KEYS:
|
||||||
|
if key in event_to_idx:
|
||||||
|
start_idx = int(event_to_idx[key])
|
||||||
|
break
|
||||||
|
|
||||||
|
return TransitionData(
|
||||||
|
human_T=human_T,
|
||||||
|
agent_T=agent_T,
|
||||||
|
terminal_mask=terminal_mask,
|
||||||
|
purchase_mask=purchase_mask,
|
||||||
|
event_weights=event_weights,
|
||||||
|
event_names=event_names,
|
||||||
|
start_idx=start_idx,
|
||||||
|
term_idx=term_idx,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def fallback_transition_data() -> TransitionData:
|
||||||
|
human = {
|
||||||
|
"session_start": {
|
||||||
|
"page_view": 0.80,
|
||||||
|
"view_item_page": 0.15,
|
||||||
|
"session_end": 0.05,
|
||||||
|
},
|
||||||
|
"page_view": {"view_item_page": 0.55, "search": 0.25, "session_end": 0.20},
|
||||||
|
"view_item_page": {
|
||||||
|
"learn_more_about_item": 0.40,
|
||||||
|
"add_item_to_cart": 0.28,
|
||||||
|
"session_end": 0.32,
|
||||||
|
},
|
||||||
|
"learn_more_about_item": {
|
||||||
|
"add_item_to_cart": 0.50,
|
||||||
|
"view_item_page": 0.30,
|
||||||
|
"session_end": 0.20,
|
||||||
|
},
|
||||||
|
"add_item_to_cart": {
|
||||||
|
"checkout_start": 0.58,
|
||||||
|
"view_item_page": 0.24,
|
||||||
|
"session_end": 0.18,
|
||||||
|
},
|
||||||
|
"checkout_start": {"purchase_complete": 0.70, "session_end": 0.30},
|
||||||
|
"purchase_complete": {"session_end": 1.0},
|
||||||
|
}
|
||||||
|
agent = {
|
||||||
|
"session_start": {
|
||||||
|
"page_view": 0.90,
|
||||||
|
"view_item_page": 0.08,
|
||||||
|
"session_end": 0.02,
|
||||||
|
},
|
||||||
|
"page_view": {"view_item_page": 0.40, "search": 0.35, "session_end": 0.25},
|
||||||
|
"view_item_page": {
|
||||||
|
"learn_more_about_item": 0.55,
|
||||||
|
"add_item_to_cart": 0.15,
|
||||||
|
"session_end": 0.30,
|
||||||
|
},
|
||||||
|
"learn_more_about_item": {
|
||||||
|
"view_item_page": 0.45,
|
||||||
|
"add_item_to_cart": 0.20,
|
||||||
|
"session_end": 0.35,
|
||||||
|
},
|
||||||
|
"add_item_to_cart": {
|
||||||
|
"checkout_start": 0.42,
|
||||||
|
"view_item_page": 0.28,
|
||||||
|
"session_end": 0.30,
|
||||||
|
},
|
||||||
|
"checkout_start": {"purchase_complete": 0.52, "session_end": 0.48},
|
||||||
|
"purchase_complete": {"session_end": 1.0},
|
||||||
|
}
|
||||||
|
return compile_transition_data(human, agent)
|
||||||
|
|
||||||
|
|
||||||
|
def load_transition_data(prefer_data: bool = True) -> TransitionData:
|
||||||
|
if not prefer_data:
|
||||||
|
return fallback_transition_data()
|
||||||
|
try:
|
||||||
|
from ..lib.behavior import get_transition_models
|
||||||
|
|
||||||
|
human_trans, agent_trans = get_transition_models()
|
||||||
|
return compile_transition_data(human_trans, agent_trans)
|
||||||
|
except Exception:
|
||||||
|
return fallback_transition_data()
|
||||||
|
|
||||||
|
|
||||||
|
if JAX_AVAILABLE:
|
||||||
|
|
||||||
|
@partial(jax.jit, static_argnums=(8, 9, 10))
|
||||||
|
def _sample_sessions_jax(
|
||||||
|
key: jax.Array,
|
||||||
|
human_T: jax.Array,
|
||||||
|
agent_T: jax.Array,
|
||||||
|
terminal_mask: jax.Array,
|
||||||
|
start_idx: int,
|
||||||
|
term_idx: int,
|
||||||
|
alpha: float,
|
||||||
|
n_products: int,
|
||||||
|
n_sessions: int,
|
||||||
|
max_steps: int,
|
||||||
|
n_states: int,
|
||||||
|
) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]:
|
||||||
|
k_actor, k_product, k_step = jax.random.split(key, 3)
|
||||||
|
start_idx_i32 = jnp.asarray(start_idx, dtype=jnp.int32)
|
||||||
|
term_idx_i32 = jnp.asarray(term_idx, dtype=jnp.int32)
|
||||||
|
actor_draw = jax.random.uniform(k_actor, (n_sessions,))
|
||||||
|
actors = (actor_draw < alpha).astype(jnp.int32)
|
||||||
|
products = jax.random.randint(
|
||||||
|
k_product, (n_sessions,), 0, n_products, dtype=jnp.int32
|
||||||
|
)
|
||||||
|
|
||||||
|
active_init = jnp.ones((n_sessions,), dtype=jnp.bool_)
|
||||||
|
state_init = jnp.full((n_sessions,), start_idx_i32, dtype=jnp.int32)
|
||||||
|
|
||||||
|
def _scan_step(carry, _):
|
||||||
|
states, active, rng = carry
|
||||||
|
rng, k = jax.random.split(rng)
|
||||||
|
probs_h = human_T[states]
|
||||||
|
probs_a = agent_T[states]
|
||||||
|
probs = jnp.where(actors[:, None] == 0, probs_h, probs_a)
|
||||||
|
next_state = jax.random.categorical(k, jnp.log(probs + 1e-10), axis=-1)
|
||||||
|
next_state = jnp.where(active, next_state, term_idx_i32)
|
||||||
|
emitted = jnp.where(active, next_state, -1)
|
||||||
|
is_terminal = terminal_mask[jnp.clip(next_state, 0, n_states - 1)]
|
||||||
|
next_active = active & (~is_terminal)
|
||||||
|
carry_states = jnp.where(next_active, next_state, term_idx_i32)
|
||||||
|
return (carry_states, next_active, rng), emitted
|
||||||
|
|
||||||
|
_, state_t = jax.lax.scan(
|
||||||
|
_scan_step, (state_init, active_init, k_step), None, length=max_steps
|
||||||
|
)
|
||||||
|
states = state_t.T
|
||||||
|
lengths = jnp.sum(states >= 0, axis=1, dtype=jnp.int32)
|
||||||
|
return states, products, actors, lengths
|
||||||
|
|
||||||
|
|
||||||
|
def sample_sessions(
|
||||||
|
key,
|
||||||
|
transition_data: TransitionData,
|
||||||
|
alpha: float,
|
||||||
|
n_products: int,
|
||||||
|
n_sessions: int,
|
||||||
|
max_steps: int,
|
||||||
|
) -> SessionBatch:
|
||||||
|
if JAX_AVAILABLE:
|
||||||
|
td = transition_data.to_jax()
|
||||||
|
states, products, actors, lengths = _sample_sessions_jax(
|
||||||
|
key,
|
||||||
|
td.human_T,
|
||||||
|
td.agent_T,
|
||||||
|
td.terminal_mask,
|
||||||
|
int(td.start_idx),
|
||||||
|
int(td.term_idx),
|
||||||
|
float(alpha),
|
||||||
|
int(n_products),
|
||||||
|
int(n_sessions),
|
||||||
|
int(max_steps),
|
||||||
|
int(td.human_T.shape[0]),
|
||||||
|
)
|
||||||
|
return SessionBatch(
|
||||||
|
states=states, products=products, actors=actors, lengths=lengths
|
||||||
|
)
|
||||||
|
|
||||||
|
rng = np.random.default_rng(int(np.asarray(key).reshape(-1)[0]))
|
||||||
|
n_states = transition_data.human_T.shape[0]
|
||||||
|
products = rng.integers(0, n_products, size=n_sessions, dtype=np.int32)
|
||||||
|
actors = (rng.random(size=n_sessions) < alpha).astype(np.int32)
|
||||||
|
states = np.full((n_sessions, max_steps), -1, dtype=np.int32)
|
||||||
|
lengths = np.zeros((n_sessions,), dtype=np.int32)
|
||||||
|
for i in range(n_sessions):
|
||||||
|
current = int(transition_data.start_idx)
|
||||||
|
mat = transition_data.agent_T if actors[i] == 1 else transition_data.human_T
|
||||||
|
for t in range(max_steps):
|
||||||
|
nxt = int(rng.choice(n_states, p=mat[current]))
|
||||||
|
states[i, t] = nxt
|
||||||
|
if transition_data.terminal_mask[nxt]:
|
||||||
|
lengths[i] = t + 1
|
||||||
|
break
|
||||||
|
current = nxt
|
||||||
|
if lengths[i] == 0:
|
||||||
|
lengths[i] = max_steps
|
||||||
|
return SessionBatch(
|
||||||
|
states=states, products=products, actors=actors, lengths=lengths
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if JAX_AVAILABLE:
|
||||||
|
|
||||||
|
@partial(jax.jit, static_argnums=(2,))
|
||||||
|
def compute_session_transitions(states, lengths, n_states: int):
|
||||||
|
src = states[:, :-1]
|
||||||
|
dst = states[:, 1:]
|
||||||
|
time_idx = jnp.arange(src.shape[1])[None, :]
|
||||||
|
valid = (src >= 0) & (dst >= 0) & (time_idx < (lengths[:, None] - 1))
|
||||||
|
src_clip = jnp.clip(src, 0, n_states - 1)
|
||||||
|
dst_clip = jnp.clip(dst, 0, n_states - 1)
|
||||||
|
src_oh = jax.nn.one_hot(src_clip, n_states)
|
||||||
|
dst_oh = jax.nn.one_hot(dst_clip, n_states)
|
||||||
|
counts = jnp.einsum(
|
||||||
|
"nti,ntj,nt->nij", src_oh, dst_oh, valid.astype(jnp.float32)
|
||||||
|
)
|
||||||
|
row_sums = jnp.sum(counts, axis=-1, keepdims=True)
|
||||||
|
return counts / (row_sums + 1e-10)
|
||||||
|
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def compute_session_transitions(states, lengths, n_states: int):
|
||||||
|
trans = np.zeros((states.shape[0], n_states, n_states), dtype=np.float32)
|
||||||
|
for i in range(states.shape[0]):
|
||||||
|
for t in range(max(int(lengths[i]) - 1, 0)):
|
||||||
|
s = int(states[i, t])
|
||||||
|
d = int(states[i, t + 1])
|
||||||
|
if s >= 0 and d >= 0:
|
||||||
|
trans[i, s, d] += 1.0
|
||||||
|
row_sums = trans.sum(axis=-1, keepdims=True)
|
||||||
|
return trans / (row_sums + 1e-10)
|
||||||
|
|
||||||
|
|
||||||
|
def batch_kl(P, Q_human, Q_agent, eps: float = 1e-10):
|
||||||
|
p = P + eps
|
||||||
|
p = p / jnp.sum(p, axis=-1, keepdims=True)
|
||||||
|
qh = Q_human[None, ...] + eps
|
||||||
|
qa = Q_agent[None, ...] + eps
|
||||||
|
delta_h = jnp.sum(p * jnp.log(p / qh), axis=(1, 2))
|
||||||
|
delta_a = jnp.sum(p * jnp.log(p / qa), axis=(1, 2))
|
||||||
|
return delta_h, delta_a
|
||||||
|
|
||||||
|
|
||||||
|
if JAX_AVAILABLE:
|
||||||
|
batch_kl = jax.jit(batch_kl)
|
||||||
|
|
||||||
|
|
||||||
|
def agent_probability_from_kl(delta_h, delta_a, temperature: float = 1.0):
|
||||||
|
t = jnp.maximum(float(temperature), 1e-6)
|
||||||
|
exp_h = jnp.exp(-delta_h / t)
|
||||||
|
exp_a = jnp.exp(-delta_a / t)
|
||||||
|
return exp_a / (exp_h + exp_a + 1e-10)
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_alpha_from_kl(delta_h, delta_a, beta: float = 2.0):
|
||||||
|
logits = beta * (delta_h - delta_a)
|
||||||
|
return 1.0 / (1.0 + jnp.exp(-logits))
|
||||||
|
|
||||||
|
|
||||||
|
def weighted_demand(states, products, n_products: int, event_weights):
|
||||||
|
valid = states >= 0
|
||||||
|
state_clip = jnp.clip(states, 0, event_weights.shape[0] - 1)
|
||||||
|
weights = event_weights[state_clip] * valid
|
||||||
|
per_session = jnp.sum(weights, axis=1)
|
||||||
|
demand = jnp.zeros((n_products,), dtype=jnp.float32)
|
||||||
|
demand = demand.at[products].add(per_session)
|
||||||
|
total = jnp.sum(demand)
|
||||||
|
return jnp.where(total > 0.0, (demand / total) * 100.0, demand)
|
||||||
|
|
||||||
|
|
||||||
|
if JAX_AVAILABLE:
|
||||||
|
weighted_demand = jax.jit(weighted_demand, static_argnums=(2,))
|
||||||
|
|
||||||
|
|
||||||
|
def purchase_flags(states, purchase_mask):
|
||||||
|
state_clip = jnp.clip(states, 0, purchase_mask.shape[0] - 1)
|
||||||
|
hits = purchase_mask[state_clip] & (states >= 0)
|
||||||
|
return jnp.any(hits, axis=1)
|
||||||
|
|
||||||
|
|
||||||
|
if JAX_AVAILABLE:
|
||||||
|
purchase_flags = jax.jit(purchase_flags)
|
||||||
|
|
||||||
|
|
||||||
|
def revenue_from_demand(prices, demand):
|
||||||
|
return jnp.dot(prices, demand)
|
||||||
|
|
||||||
|
|
||||||
|
if JAX_AVAILABLE:
|
||||||
|
revenue_from_demand = jax.jit(revenue_from_demand)
|
||||||
|
|
||||||
|
|
||||||
|
def reward_with_coi_penalty(
|
||||||
|
revenue, agent_prob: float, lambda_coi: float, info_value: float
|
||||||
|
):
|
||||||
|
leakage = agent_prob * info_value
|
||||||
|
discount = jnp.clip(1.0 - lambda_coi * leakage, 0.0, 1.0)
|
||||||
|
return revenue * discount, leakage, discount
|
||||||
|
|
||||||
|
|
||||||
|
if JAX_AVAILABLE:
|
||||||
|
reward_with_coi_penalty = jax.jit(reward_with_coi_penalty)
|
||||||
5
engine/jax/requirements.txt
Normal file
5
engine/jax/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
flax==0.10.7
|
||||||
|
optax==0.2.7
|
||||||
|
distrax==0.1.5
|
||||||
|
orbax-checkpoint==0.11.32
|
||||||
|
chex==0.1.90
|
||||||
1304
engine/jax/train.py
Normal file
1304
engine/jax/train.py
Normal file
File diff suppressed because it is too large
Load Diff
14
engine/lib/__init__.py
Normal file
14
engine/lib/__init__.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
from .demand import estimate_demand, estimate_weighted_demand, generate_demand_for_actor
|
||||||
|
from .behavior import sample_behavior, get_transition_models, trajectory_to_events
|
||||||
|
from .render import DashboardRenderer, style_axis
|
||||||
|
from .wrappers import EconomicMetricsWrapper
|
||||||
|
from .callbacks import MetricsCallback, EvalMetricsCallback, CheckpointArtifactCallback
|
||||||
|
from .providers import (
|
||||||
|
ProviderBenchmark,
|
||||||
|
ProviderResult,
|
||||||
|
BenchmarkConfig,
|
||||||
|
RandomBaseline,
|
||||||
|
SurgeBaseline,
|
||||||
|
)
|
||||||
|
from .coi import compute_uplift_coi, extract_purchases, compute_agent_probability
|
||||||
|
from .discrete import EventQTable
|
||||||
134
engine/lib/behavior.py
Normal file
134
engine/lib/behavior.py
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
sys.path.insert(0, str(Path(__file__).parents[2]))
|
||||||
|
|
||||||
|
try:
|
||||||
|
from sim.rl.behavior_loader.models import (
|
||||||
|
BehaviorModel,
|
||||||
|
AgentBehaviorModel,
|
||||||
|
aggregate_event_transitions,
|
||||||
|
)
|
||||||
|
except ImportError:
|
||||||
|
BehaviorModel = None
|
||||||
|
AgentBehaviorModel = None
|
||||||
|
aggregate_event_transitions = None
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
from .demand import generate_demand_for_actor
|
||||||
|
|
||||||
|
base_dir = Path(__file__).parents[2] / "experiments"
|
||||||
|
human_dir = str(base_dir / "collected_data")
|
||||||
|
agent_dir = str(base_dir / "agents" / "collected_data")
|
||||||
|
|
||||||
|
_cache = {} # lazy cache for models and base pivots
|
||||||
|
|
||||||
|
|
||||||
|
def _get_base_pivot(human: bool):
|
||||||
|
if (
|
||||||
|
BehaviorModel is None
|
||||||
|
or AgentBehaviorModel is None
|
||||||
|
or aggregate_event_transitions is None
|
||||||
|
):
|
||||||
|
raise ImportError("behavior loader dependencies are unavailable")
|
||||||
|
key = "human" if human else "agent"
|
||||||
|
if key not in _cache:
|
||||||
|
model = BehaviorModel(human_dir) if human else AgentBehaviorModel(agent_dir)
|
||||||
|
mdp = model.build_MDP()
|
||||||
|
_cache[key] = pd.DataFrame(aggregate_event_transitions(mdp)).fillna(0.0)
|
||||||
|
return _cache[key]
|
||||||
|
|
||||||
|
|
||||||
|
def get_transition_models():
|
||||||
|
"""load human and agent transition models for agent probability calculation
|
||||||
|
|
||||||
|
returns:
|
||||||
|
tuple: (human_transitions, agent_transitions) as dicts of event->event->prob
|
||||||
|
"""
|
||||||
|
if (
|
||||||
|
BehaviorModel is None
|
||||||
|
or AgentBehaviorModel is None
|
||||||
|
or aggregate_event_transitions is None
|
||||||
|
):
|
||||||
|
raise ImportError("behavior loader dependencies are unavailable")
|
||||||
|
|
||||||
|
human_model = BehaviorModel(human_dir)
|
||||||
|
agent_model = AgentBehaviorModel(agent_dir)
|
||||||
|
|
||||||
|
human_mdp = human_model.build_MDP()
|
||||||
|
agent_mdp = agent_model.build_MDP()
|
||||||
|
|
||||||
|
human_trans = aggregate_event_transitions(human_mdp)
|
||||||
|
agent_trans = aggregate_event_transitions(agent_mdp)
|
||||||
|
|
||||||
|
return human_trans, agent_trans
|
||||||
|
|
||||||
|
|
||||||
|
def trajectory_to_events(trajectory: list) -> list:
|
||||||
|
"""extract event names from trajectory for KL divergence calculation
|
||||||
|
|
||||||
|
trajectories are in format 'eventName_product0', extract just eventName
|
||||||
|
|
||||||
|
args:
|
||||||
|
trajectory: list like ['view_product0', 'add_to_cart_product1', 'checkout_product1']
|
||||||
|
|
||||||
|
returns:
|
||||||
|
list: event names like ['view', 'add_to_cart', 'checkout']
|
||||||
|
"""
|
||||||
|
events = []
|
||||||
|
for state in trajectory:
|
||||||
|
# state format from sample_behavior: 'eventName_productX'
|
||||||
|
if "_product" in state:
|
||||||
|
event = state.rsplit("_product", 1)[0]
|
||||||
|
else:
|
||||||
|
event = state
|
||||||
|
events.append(event)
|
||||||
|
return events
|
||||||
|
|
||||||
|
|
||||||
|
def adjust_behavior_to_condition(condition, transition_matrix):
|
||||||
|
# expand NxN transition matrix to (N*P)x(N*P) weighted by demand condition
|
||||||
|
condition = np.asarray(condition, dtype=float)
|
||||||
|
condition = np.nan_to_num(condition, nan=0.0, posinf=0.0, neginf=0.0)
|
||||||
|
condition = np.clip(condition, 0.0, None)
|
||||||
|
s = float(np.sum(condition))
|
||||||
|
if not np.isfinite(s) or s <= 0:
|
||||||
|
cond_norm = np.full(len(condition), 1.0 / max(len(condition), 1), dtype=float)
|
||||||
|
else:
|
||||||
|
cond_norm = condition / s
|
||||||
|
n_products = len(condition)
|
||||||
|
base_vals = transition_matrix.values
|
||||||
|
base_cols, base_rows = (
|
||||||
|
transition_matrix.columns.tolist(),
|
||||||
|
transition_matrix.index.tolist(),
|
||||||
|
)
|
||||||
|
|
||||||
|
# expand via kronecker-like tiling: each cell becomes a P*P block weighted by outer product of cond_norm
|
||||||
|
expanded = np.kron(base_vals, np.outer(cond_norm, cond_norm))
|
||||||
|
new_cols = [f"{c}_product{p}" for c in base_cols for p in range(n_products)]
|
||||||
|
new_rows = [f"{r}_product{p}" for r in base_rows for p in range(n_products)]
|
||||||
|
return pd.DataFrame(expanded, index=new_rows, columns=new_cols)
|
||||||
|
|
||||||
|
|
||||||
|
def sample_behavior(condition, human=True, max_len=40):
|
||||||
|
base_pivot = _get_base_pivot(human)
|
||||||
|
adjusted_transitions = adjust_behavior_to_condition(condition, base_pivot)
|
||||||
|
|
||||||
|
trajectory = [np.random.choice(adjusted_transitions.index)]
|
||||||
|
while len(trajectory) < max_len and "checkout" not in trajectory[-1]:
|
||||||
|
probs = np.asarray(adjusted_transitions.loc[trajectory[-1]].values, dtype=float)
|
||||||
|
probs = np.nan_to_num(probs, nan=0.0, posinf=0.0, neginf=0.0)
|
||||||
|
probs = np.clip(probs, 0.0, None)
|
||||||
|
s = float(np.sum(probs))
|
||||||
|
sample = np.random.choice(
|
||||||
|
adjusted_transitions.columns, p=(probs / s) if s > 0 else None
|
||||||
|
)
|
||||||
|
trajectory.append(sample)
|
||||||
|
return trajectory
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
t = sample_behavior(generate_demand_for_actor(np.array([10, 20, 30])), human=True)
|
||||||
|
print(t)
|
||||||
|
t = sample_behavior(generate_demand_for_actor(np.array([10, 20, 30])), human=False)
|
||||||
|
print(t)
|
||||||
182
engine/lib/callbacks.py
Normal file
182
engine/lib/callbacks.py
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
"""Training callbacks for W&B/TensorBoard logging - reads from info dict."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from stable_baselines3.common.callbacks import BaseCallback, EvalCallback
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from ..wandb_checkpoint import checkpoint_artifact_name, log_checkpoint_file
|
||||||
|
|
||||||
|
try:
|
||||||
|
import wandb
|
||||||
|
|
||||||
|
HAS_WANDB = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_WANDB = False
|
||||||
|
|
||||||
|
|
||||||
|
class MetricsCallback(BaseCallback):
|
||||||
|
"""Training metrics logger - reads info['economics'], logs to W&B."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, log_histograms: bool = True, log_freq: int = 100, verbose: int = 0
|
||||||
|
):
|
||||||
|
super().__init__(verbose)
|
||||||
|
self.log_histograms = log_histograms
|
||||||
|
self.log_freq = log_freq
|
||||||
|
self._episode_revenues: list[float] = []
|
||||||
|
|
||||||
|
def _on_step(self) -> bool:
|
||||||
|
if not HAS_WANDB or wandb.run is None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
for info in self.locals.get("infos", []):
|
||||||
|
if "economics" not in info:
|
||||||
|
continue
|
||||||
|
|
||||||
|
econ = info["economics"]
|
||||||
|
t = self.num_timesteps
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"economics/revenue": econ["revenue"],
|
||||||
|
"economics/margin": econ["margin"],
|
||||||
|
"coi/level": econ["coi_level"],
|
||||||
|
"economics/regret": econ["regret"],
|
||||||
|
}
|
||||||
|
if "coi_mix" in econ:
|
||||||
|
payload["coi/mix"] = econ["coi_mix"]
|
||||||
|
if "coi_base" in econ:
|
||||||
|
payload["coi/base"] = econ["coi_base"]
|
||||||
|
if "coi_leakage" in econ:
|
||||||
|
payload["coi/leakage"] = econ["coi_leakage"]
|
||||||
|
if "coi_penalty" in econ:
|
||||||
|
payload["coi/penalty"] = econ["coi_penalty"]
|
||||||
|
wandb.log(payload, step=t)
|
||||||
|
|
||||||
|
self._episode_revenues.append(econ["revenue"])
|
||||||
|
|
||||||
|
# histograms at log_freq intervals
|
||||||
|
if self.log_histograms and self.num_timesteps % self.log_freq == 0:
|
||||||
|
for info in self.locals.get("infos", []):
|
||||||
|
if "prices" in info:
|
||||||
|
wandb.log(
|
||||||
|
{"distributions/prices": wandb.Histogram(info["prices"])},
|
||||||
|
step=self.num_timesteps,
|
||||||
|
)
|
||||||
|
if "demand" in info:
|
||||||
|
wandb.log(
|
||||||
|
{"distributions/demand": wandb.Histogram(info["demand"])},
|
||||||
|
step=self.num_timesteps,
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _on_rollout_end(self) -> None:
|
||||||
|
if not HAS_WANDB or wandb.run is None or not self._episode_revenues:
|
||||||
|
return
|
||||||
|
wandb.log(
|
||||||
|
{
|
||||||
|
"episode/mean_revenue": np.mean(self._episode_revenues),
|
||||||
|
"episode/total_revenue": np.sum(self._episode_revenues),
|
||||||
|
},
|
||||||
|
step=self.num_timesteps,
|
||||||
|
)
|
||||||
|
self._episode_revenues = []
|
||||||
|
|
||||||
|
|
||||||
|
class CheckpointArtifactCallback(BaseCallback):
|
||||||
|
"""Periodic SB3 checkpoint uploader backed by W&B artifacts."""
|
||||||
|
|
||||||
|
def __init__(self, cfg: dict, interval: int = 10_000, verbose: int = 0):
|
||||||
|
super().__init__(verbose)
|
||||||
|
self.cfg = dict(cfg)
|
||||||
|
self.interval = max(1, int(interval))
|
||||||
|
self.model_dir = Path(str(self.cfg.get("model_dir", "engine/models")))
|
||||||
|
self.model_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self._next_checkpoint = self.interval
|
||||||
|
self._last_saved_step = -1
|
||||||
|
|
||||||
|
def _artifact_name(self) -> str:
|
||||||
|
sweep_id = (
|
||||||
|
getattr(wandb.run, "sweep_id", None)
|
||||||
|
if HAS_WANDB and wandb.run is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
return checkpoint_artifact_name(self.cfg, backend="sb3", sweep_id=sweep_id)
|
||||||
|
|
||||||
|
def _checkpoint_file(self) -> Path:
|
||||||
|
algo = str(self.cfg.get("algo", "model"))
|
||||||
|
base = self.model_dir / f"phantom_{algo}_checkpoint"
|
||||||
|
self.model.save(str(base))
|
||||||
|
return base.with_suffix(".zip")
|
||||||
|
|
||||||
|
def _save_checkpoint(self) -> None:
|
||||||
|
if not HAS_WANDB or wandb.run is None:
|
||||||
|
return
|
||||||
|
step = int(self.num_timesteps)
|
||||||
|
if step <= self._last_saved_step:
|
||||||
|
return
|
||||||
|
checkpoint_path = self._checkpoint_file()
|
||||||
|
metadata = {
|
||||||
|
"step": step,
|
||||||
|
"algo": str(self.cfg.get("algo", "unknown")),
|
||||||
|
"sweep_id": getattr(wandb.run, "sweep_id", None),
|
||||||
|
}
|
||||||
|
saved = log_checkpoint_file(
|
||||||
|
self._artifact_name(),
|
||||||
|
file_path=checkpoint_path,
|
||||||
|
artifact_file_name=checkpoint_path.name,
|
||||||
|
metadata=metadata,
|
||||||
|
)
|
||||||
|
if saved:
|
||||||
|
self._last_saved_step = step
|
||||||
|
|
||||||
|
def _on_step(self) -> bool:
|
||||||
|
if self.num_timesteps < self._next_checkpoint:
|
||||||
|
return True
|
||||||
|
self._save_checkpoint()
|
||||||
|
while self._next_checkpoint <= self.num_timesteps:
|
||||||
|
self._next_checkpoint += self.interval
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _on_training_end(self) -> None:
|
||||||
|
self._save_checkpoint()
|
||||||
|
|
||||||
|
|
||||||
|
class EvalMetricsCallback(EvalCallback):
|
||||||
|
"""Deterministic evaluation - true performance without exploration noise."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, eval_env, eval_freq: int = 1000, n_eval_episodes: int = 5, **kwargs
|
||||||
|
):
|
||||||
|
super().__init__(
|
||||||
|
eval_env, eval_freq=eval_freq, n_eval_episodes=n_eval_episodes, **kwargs
|
||||||
|
)
|
||||||
|
self._eval_revenues: list[float] = []
|
||||||
|
|
||||||
|
def _on_step(self) -> bool:
|
||||||
|
result = super()._on_step()
|
||||||
|
|
||||||
|
if not HAS_WANDB or wandb.run is None:
|
||||||
|
return result
|
||||||
|
|
||||||
|
# log eval metrics after evaluation runs
|
||||||
|
if self.n_calls % self.eval_freq == 0 and hasattr(self, "last_mean_reward"):
|
||||||
|
wandb.log(
|
||||||
|
{
|
||||||
|
"eval/mean_reward": self.last_mean_reward,
|
||||||
|
"eval/mean_revenue": np.mean(self._eval_revenues)
|
||||||
|
if self._eval_revenues
|
||||||
|
else 0,
|
||||||
|
},
|
||||||
|
step=self.num_timesteps,
|
||||||
|
)
|
||||||
|
self._eval_revenues = []
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _log_success_callback(self, locals_: dict, globals_: dict) -> None:
|
||||||
|
# called after each eval episode
|
||||||
|
info = locals_.get("info", {})
|
||||||
|
if "economics" in info:
|
||||||
|
self._eval_revenues.append(info["economics"]["revenue"])
|
||||||
76
engine/lib/coi.py
Normal file
76
engine/lib/coi.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import numpy as np
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
def compute_agent_probability(
|
||||||
|
trajectory: list, human_transitions: Dict, agent_transitions: Dict
|
||||||
|
) -> float:
|
||||||
|
"""estimate agent probability via KL divergence between trajectory transitions and reference models
|
||||||
|
|
||||||
|
compares empirical trajectory transition distribution to human/agent prototypes
|
||||||
|
|
||||||
|
args:
|
||||||
|
trajectory: list of state/event strings from session
|
||||||
|
human_transitions: reference transition dict from human MDP (event->event->prob)
|
||||||
|
agent_transitions: reference transition dict from agent MDP (event->event->prob)
|
||||||
|
|
||||||
|
returns:
|
||||||
|
agent probability in [0, 1] via softmax over KL divergences
|
||||||
|
"""
|
||||||
|
if len(trajectory) < 2:
|
||||||
|
return 0.0 # insufficient data, assume human
|
||||||
|
|
||||||
|
# build empirical transition distribution from trajectory
|
||||||
|
trans_counts = {}
|
||||||
|
for s, s_next in zip(trajectory[:-1], trajectory[1:]):
|
||||||
|
if s not in trans_counts:
|
||||||
|
trans_counts[s] = {}
|
||||||
|
trans_counts[s][s_next] = trans_counts[s].get(s_next, 0) + 1
|
||||||
|
|
||||||
|
# normalize to probabilities
|
||||||
|
empirical = {}
|
||||||
|
for s, nxt in trans_counts.items():
|
||||||
|
total = sum(nxt.values())
|
||||||
|
empirical[s] = {s_n: cnt / total for s_n, cnt in nxt.items()}
|
||||||
|
|
||||||
|
# compute KL divergence to each prototype
|
||||||
|
def kl_div(p_dist: Dict, q_dist: Dict) -> float:
|
||||||
|
eps = 1e-10
|
||||||
|
# aggregate over all source states in empirical dist
|
||||||
|
kl = 0.0
|
||||||
|
for s in p_dist:
|
||||||
|
if s not in q_dist:
|
||||||
|
continue # skip states not in reference
|
||||||
|
p_trans, q_trans = p_dist[s], q_dist[s]
|
||||||
|
for k in p_trans:
|
||||||
|
p_val = p_trans[k] + eps
|
||||||
|
q_val = q_trans.get(k, 0.0) + eps
|
||||||
|
kl += p_val * np.log(p_val / q_val)
|
||||||
|
return kl
|
||||||
|
|
||||||
|
kl_human = kl_div(empirical, human_transitions)
|
||||||
|
kl_agent = kl_div(empirical, agent_transitions)
|
||||||
|
|
||||||
|
# convert to probability via softmax (lower KL = higher prob)
|
||||||
|
# agent_prob = exp(-kl_agent) / (exp(-kl_human) + exp(-kl_agent))
|
||||||
|
exp_h = np.exp(-kl_human)
|
||||||
|
exp_a = np.exp(-kl_agent)
|
||||||
|
return float(exp_a / (exp_h + exp_a + 1e-10))
|
||||||
|
|
||||||
|
|
||||||
|
def extract_purchases(trajectories: list) -> Dict[int, int]:
|
||||||
|
purchases: Dict[int, int] = {}
|
||||||
|
for traj in trajectories:
|
||||||
|
if traj and "checkout" in traj[-1] and "_product" in traj[-1]:
|
||||||
|
prod_id = int(traj[-1].rsplit("_product", 1)[1])
|
||||||
|
purchases[prod_id] = purchases.get(prod_id, 0) + 1
|
||||||
|
return purchases
|
||||||
|
|
||||||
|
|
||||||
|
def compute_uplift_coi(
|
||||||
|
prices: np.ndarray, purchases: Dict[int, int], baseline_prices: np.ndarray
|
||||||
|
) -> float:
|
||||||
|
# TODO: consider view-weighted fractional purchase for denser signal
|
||||||
|
return float(
|
||||||
|
sum(max(0.0, prices[k] - baseline_prices[k]) * n for k, n in purchases.items())
|
||||||
|
)
|
||||||
92
engine/lib/demand.py
Normal file
92
engine/lib/demand.py
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import numpy as np
|
||||||
|
|
||||||
|
CATEGORY_WEIGHTS = {"cart": 4.0, "dwell": 2.0, "nav": 1.0, "filter": 0.5}
|
||||||
|
ACTION_CATEGORIES = {
|
||||||
|
"cart": {"add_item", "add_to_cart", "remove", "checkout", "purchase"},
|
||||||
|
"dwell": {"hover_title", "hover_paragraph", "hover_link"},
|
||||||
|
"nav": {"page_view", "view_item", "view", "learn_more"},
|
||||||
|
"filter": {"search", "filter_date", "filter_price", "sort"},
|
||||||
|
}
|
||||||
|
DEFAULT_ACTION_WEIGHTS = {
|
||||||
|
a: CATEGORY_WEIGHTS[c] for c, actions in ACTION_CATEGORIES.items() for a in actions
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def generate_demand_for_actor(
|
||||||
|
prices: np.ndarray,
|
||||||
|
params: tuple,
|
||||||
|
noise_std: float = 1.0,
|
||||||
|
distribution_method=np.random.normal,
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""d(p;0) = max(0, valuation - price) + epsi for single actor type
|
||||||
|
params: (mean, std) for valuation distribution D_H or D_A"""
|
||||||
|
val = distribution_method(*params, size=len(prices))
|
||||||
|
noise = distribution_method(0, noise_std, len(prices))
|
||||||
|
demand = np.maximum(0, val - prices + noise)
|
||||||
|
total = np.sum(demand)
|
||||||
|
return demand / total * 100 if total > 0 else demand
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_demand(trajectories, action_weights=None):
|
||||||
|
return estimate_weighted_demand(trajectories, action_weights)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_event_state(state: str):
|
||||||
|
if "_product" not in state:
|
||||||
|
return state, None
|
||||||
|
action, raw_pid = state.rsplit("_product", 1)
|
||||||
|
return action, int(raw_pid) if raw_pid.isdigit() else None
|
||||||
|
|
||||||
|
|
||||||
|
def _weight_for_action(action: str, action_weights: dict) -> float:
|
||||||
|
if action in action_weights:
|
||||||
|
return action_weights[action]
|
||||||
|
if action.startswith("hover"):
|
||||||
|
return CATEGORY_WEIGHTS["dwell"]
|
||||||
|
if action.startswith("filter") or action in {"search", "sort"}:
|
||||||
|
return CATEGORY_WEIGHTS["filter"]
|
||||||
|
if action.startswith("add") or action in {"checkout", "purchase", "remove"}:
|
||||||
|
return CATEGORY_WEIGHTS["cart"]
|
||||||
|
return CATEGORY_WEIGHTS["nav"]
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_weighted_demand(trajectories, action_weights=None):
|
||||||
|
action_weights = (
|
||||||
|
DEFAULT_ACTION_WEIGHTS if action_weights is None else action_weights
|
||||||
|
)
|
||||||
|
scores = {}
|
||||||
|
for traj in trajectories:
|
||||||
|
for state in traj:
|
||||||
|
action, product_id = _parse_event_state(state)
|
||||||
|
if product_id is None:
|
||||||
|
continue
|
||||||
|
w = _weight_for_action(action, action_weights)
|
||||||
|
if w <= 0:
|
||||||
|
continue
|
||||||
|
scores[product_id] = scores.get(product_id, 0.0) + w
|
||||||
|
total = sum(scores.values())
|
||||||
|
return (
|
||||||
|
{pid: (score / total) * 100 for pid, score in scores.items()}
|
||||||
|
if total > 0
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Example usage
|
||||||
|
if __name__ == "__main__":
|
||||||
|
np.random.seed(42)
|
||||||
|
prices = np.array([20.0, 35.0, 50.0, 65.0])
|
||||||
|
# demo actor-specific demands
|
||||||
|
human_params, agent_params = (50, 10), (45, 15)
|
||||||
|
demand_h = generate_demand_for_actor(prices, human_params)
|
||||||
|
demand_a = generate_demand_for_actor(prices, agent_params)
|
||||||
|
print("Human Demand:", demand_h)
|
||||||
|
print("Agent Demand:", demand_a)
|
||||||
|
from .behavior import sample_behavior
|
||||||
|
|
||||||
|
N, alpha = 200, 0.3
|
||||||
|
n_h, n_a = int(N * (1 - alpha)), int(N * alpha)
|
||||||
|
human_t = [sample_behavior(demand_h, human=True) for _ in range(n_h)]
|
||||||
|
agent_t = [sample_behavior(demand_a, human=False) for _ in range(n_a)]
|
||||||
|
demand_estimate = estimate_demand(human_t + agent_t)
|
||||||
|
print("Estimated Demand from Behavior:", demand_estimate)
|
||||||
70
engine/lib/discrete.py
Normal file
70
engine/lib/discrete.py
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
import gymnasium as gym
|
||||||
|
from gymnasium import spaces
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
class DiscretePriceActionWrapper(gym.ActionWrapper):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
env: gym.Env,
|
||||||
|
n_levels: int = 9,
|
||||||
|
min_scale: float = 0.8,
|
||||||
|
max_scale: float = 1.2,
|
||||||
|
):
|
||||||
|
super().__init__(env)
|
||||||
|
self.scales = np.linspace(min_scale, max_scale, n_levels, dtype=np.float32)
|
||||||
|
self.action_space = spaces.Discrete(n_levels)
|
||||||
|
|
||||||
|
def action(self, action: int):
|
||||||
|
scale = float(self.scales[int(action)])
|
||||||
|
cur = np.asarray(self.env.unwrapped._prices, dtype=np.float32)
|
||||||
|
lo, hi = self.env.unwrapped.price_bounds
|
||||||
|
return np.clip(cur * scale, lo, hi).astype(np.float32)
|
||||||
|
|
||||||
|
|
||||||
|
class EventQTable:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
n_actions: int,
|
||||||
|
n_products: int,
|
||||||
|
price_bounds: tuple,
|
||||||
|
lr: float = 0.1,
|
||||||
|
gamma: float = 0.99,
|
||||||
|
n_bins: int = 6,
|
||||||
|
):
|
||||||
|
self.n_actions = int(n_actions)
|
||||||
|
self.n_products = int(n_products)
|
||||||
|
self.lr = float(lr)
|
||||||
|
self.gamma = float(gamma)
|
||||||
|
self.q = defaultdict(lambda: np.zeros(self.n_actions, dtype=np.float32))
|
||||||
|
lo, hi = price_bounds
|
||||||
|
self.demand_bins = np.linspace(0.0, 100.0, n_bins + 1)[1:-1]
|
||||||
|
self.price_bins = np.linspace(lo, hi, n_bins + 1)[1:-1]
|
||||||
|
|
||||||
|
def encode(self, obs: np.ndarray) -> tuple:
|
||||||
|
obs = np.asarray(obs, dtype=np.float32)
|
||||||
|
d = obs[: self.n_products]
|
||||||
|
p = obs[self.n_products : 2 * self.n_products]
|
||||||
|
d_mean = float(np.mean(d)) if d.size else 0.0
|
||||||
|
d_std = float(np.std(d)) if d.size else 0.0
|
||||||
|
p_mean = float(np.mean(p)) if p.size else 0.0
|
||||||
|
return (
|
||||||
|
int(np.digitize(d_mean, self.demand_bins)),
|
||||||
|
int(np.digitize(d_std, self.demand_bins)),
|
||||||
|
int(np.digitize(p_mean, self.price_bins)),
|
||||||
|
)
|
||||||
|
|
||||||
|
def act(self, obs: np.ndarray, eps: float = 0.0) -> tuple[int, tuple]:
|
||||||
|
s = self.encode(obs)
|
||||||
|
if np.random.random() < eps:
|
||||||
|
return int(np.random.randint(self.n_actions)), s
|
||||||
|
return int(np.argmax(self.q[s])), s
|
||||||
|
|
||||||
|
def update(self, s: tuple, a: int, r: float, s2: tuple, done: bool):
|
||||||
|
target = r + (0.0 if done else self.gamma * float(np.max(self.q[s2])))
|
||||||
|
self.q[s][a] += self.lr * (target - self.q[s][a])
|
||||||
|
|
||||||
|
def predict(self, obs: np.ndarray, deterministic: bool = True):
|
||||||
|
a, _ = self.act(obs, 0.0 if deterministic else 0.05)
|
||||||
|
return a, None
|
||||||
182
engine/lib/providers.py
Normal file
182
engine/lib/providers.py
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
"""Provider benchmarking - compare pricing strategies across contamination levels."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Callable, Any
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
try:
|
||||||
|
import wandb
|
||||||
|
|
||||||
|
HAS_WANDB = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_WANDB = False
|
||||||
|
|
||||||
|
|
||||||
|
class RandomBaseline:
|
||||||
|
"""uniform random action selection as a lower-bound baseline"""
|
||||||
|
|
||||||
|
def __init__(self, n_actions: int):
|
||||||
|
self.n = n_actions
|
||||||
|
|
||||||
|
def __call__(self, obs):
|
||||||
|
return int(np.random.randint(self.n))
|
||||||
|
|
||||||
|
def predict(self, obs, **kw):
|
||||||
|
return self(obs), None
|
||||||
|
|
||||||
|
|
||||||
|
class SurgeBaseline:
|
||||||
|
"""heuristic surge pricing: boost price when demand is above threshold, discount when below.
|
||||||
|
matches the naive pricing rule from thesis Section 3.3.2"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, n_actions: int, high_threshold: float = 60.0, low_threshold: float = 30.0
|
||||||
|
):
|
||||||
|
self.n = n_actions
|
||||||
|
self.mid = n_actions // 2 # identity action (scale ~1.0)
|
||||||
|
self.high_t = high_threshold
|
||||||
|
self.low_t = low_threshold
|
||||||
|
|
||||||
|
def __call__(self, obs):
|
||||||
|
obs = np.asarray(obs, dtype=np.float32)
|
||||||
|
n_prod = len(obs) // 2
|
||||||
|
demand_mean = float(np.mean(obs[:n_prod])) if n_prod > 0 else 0.0
|
||||||
|
if demand_mean >= self.high_t:
|
||||||
|
return min(self.mid + 2, self.n - 1) # surge: two levels above identity
|
||||||
|
if demand_mean <= self.low_t:
|
||||||
|
return max(self.mid - 2, 0) # discount: two levels below identity
|
||||||
|
return self.mid # hold
|
||||||
|
|
||||||
|
def predict(self, obs, **kw):
|
||||||
|
return self(obs), None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ProviderResult:
|
||||||
|
"""Single benchmark result for one provider at one alpha level."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
alpha: float
|
||||||
|
total_revenue: float
|
||||||
|
mean_revenue: float
|
||||||
|
coi_level: float
|
||||||
|
coi_preserved_pct: float # vs alpha=0 baseline
|
||||||
|
margin_integrity: float
|
||||||
|
regret: float
|
||||||
|
episodes: int
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class BenchmarkConfig:
|
||||||
|
"""Configuration for provider benchmark runs."""
|
||||||
|
|
||||||
|
n_episodes: int = 100
|
||||||
|
alpha_range: list[float] = field(default_factory=lambda: [0.0, 0.1, 0.3, 0.5])
|
||||||
|
baseline_name: str = "fixed"
|
||||||
|
|
||||||
|
|
||||||
|
class ProviderBenchmark:
|
||||||
|
"""Compare pricing providers to prove margin preservation across contamination levels.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
def env_factory(alpha):
|
||||||
|
return EconomicMetricsWrapper(PHANTOM(alpha=alpha))
|
||||||
|
|
||||||
|
providers = {
|
||||||
|
"fixed": lambda obs: np.ones(10) * 50,
|
||||||
|
"learned": model.predict,
|
||||||
|
}
|
||||||
|
|
||||||
|
benchmark = ProviderBenchmark(env_factory, providers)
|
||||||
|
results = benchmark.run()
|
||||||
|
print(benchmark.summary_table())
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
env_factory: Callable[[float], Any],
|
||||||
|
providers: dict[str, Callable],
|
||||||
|
config: BenchmarkConfig | None = None,
|
||||||
|
):
|
||||||
|
self.env_factory = env_factory # fn(alpha) -> wrapped env
|
||||||
|
self.providers = providers # {name: fn(obs) -> action}
|
||||||
|
self.config = config or BenchmarkConfig()
|
||||||
|
self.results: list[ProviderResult] = []
|
||||||
|
|
||||||
|
def run(self) -> list[ProviderResult]:
|
||||||
|
"""Run benchmark across all providers and alpha levels."""
|
||||||
|
baseline_coi: dict[str, float] = {} # {provider: coi at alpha=0}
|
||||||
|
|
||||||
|
for alpha in self.config.alpha_range:
|
||||||
|
env = self.env_factory(alpha)
|
||||||
|
|
||||||
|
for name, policy_fn in self.providers.items():
|
||||||
|
revenues, coi_levels, margins = [], [], []
|
||||||
|
|
||||||
|
for _ in range(self.config.n_episodes):
|
||||||
|
obs, _ = env.reset()
|
||||||
|
episode_revenue = 0.0
|
||||||
|
done = False
|
||||||
|
|
||||||
|
while not done:
|
||||||
|
action = policy_fn(obs)
|
||||||
|
# handle sb3 model.predict returning tuple
|
||||||
|
if isinstance(action, tuple):
|
||||||
|
action = action[0]
|
||||||
|
obs, reward, term, trunc, info = env.step(action)
|
||||||
|
done = term or trunc
|
||||||
|
|
||||||
|
econ = info.get("economics", {})
|
||||||
|
episode_revenue += econ.get("revenue", 0)
|
||||||
|
coi_levels.append(econ.get("coi_level", 0))
|
||||||
|
margins.append(econ.get("margin", 0))
|
||||||
|
|
||||||
|
revenues.append(episode_revenue)
|
||||||
|
|
||||||
|
mean_coi = np.mean(coi_levels) if coi_levels else 0.0
|
||||||
|
if alpha == 0.0:
|
||||||
|
baseline_coi[name] = mean_coi
|
||||||
|
|
||||||
|
base = baseline_coi.get(name, mean_coi)
|
||||||
|
coi_preserved = mean_coi / base if base > 0 else 1.0
|
||||||
|
|
||||||
|
result = ProviderResult(
|
||||||
|
name=name,
|
||||||
|
alpha=alpha,
|
||||||
|
total_revenue=float(np.sum(revenues)),
|
||||||
|
mean_revenue=float(np.mean(revenues)),
|
||||||
|
coi_level=mean_coi,
|
||||||
|
coi_preserved_pct=coi_preserved * 100,
|
||||||
|
margin_integrity=float(np.mean(margins)) if margins else 0.0,
|
||||||
|
regret=0.0, # compute vs optimal if known
|
||||||
|
episodes=self.config.n_episodes,
|
||||||
|
)
|
||||||
|
self.results.append(result)
|
||||||
|
|
||||||
|
# log to wandb if available
|
||||||
|
if HAS_WANDB and wandb.run is not None:
|
||||||
|
wandb.log(
|
||||||
|
{
|
||||||
|
f"benchmark/{name}/revenue": result.mean_revenue,
|
||||||
|
f"benchmark/{name}/coi_preserved": result.coi_preserved_pct,
|
||||||
|
f"benchmark/{name}/margin": result.margin_integrity,
|
||||||
|
"benchmark/alpha": alpha,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.results
|
||||||
|
|
||||||
|
def to_dataframe(self) -> pd.DataFrame:
|
||||||
|
"""Convert results to pandas DataFrame."""
|
||||||
|
return pd.DataFrame([r.__dict__ for r in self.results])
|
||||||
|
|
||||||
|
def summary_table(self) -> pd.DataFrame:
|
||||||
|
"""Pivot table: providers x alpha with revenue/COI metrics."""
|
||||||
|
df = self.to_dataframe()
|
||||||
|
return df.pivot_table(
|
||||||
|
index="name",
|
||||||
|
columns="alpha",
|
||||||
|
values=["mean_revenue", "coi_preserved_pct", "margin_integrity"],
|
||||||
|
aggfunc="mean",
|
||||||
|
)
|
||||||
126
engine/lib/render.py
Normal file
126
engine/lib/render.py
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
"""rendering logic for PHANTOM environment dashboard"""
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
from matplotlib.gridspec import GridSpec
|
||||||
|
|
||||||
|
|
||||||
|
def style_axis(ax, title: str = None, xlabel: str = None, ylabel: str = None):
|
||||||
|
ax.spines['top'].set_visible(False)
|
||||||
|
ax.spines['right'].set_visible(False)
|
||||||
|
if title: ax.set_title(title, fontsize=11, fontweight='bold', pad=8)
|
||||||
|
if xlabel: ax.set_xlabel(xlabel, fontsize=9)
|
||||||
|
if ylabel: ax.set_ylabel(ylabel, fontsize=9)
|
||||||
|
|
||||||
|
|
||||||
|
class DashboardRenderer:
|
||||||
|
"""stateful renderer for PHANTOM market dynamics visualization"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.fig = None
|
||||||
|
self.gs = None
|
||||||
|
|
||||||
|
def render(self, env) -> None:
|
||||||
|
if self.fig is None:
|
||||||
|
plt.ion()
|
||||||
|
self.fig = plt.figure(figsize=(14, 10))
|
||||||
|
self.gs = GridSpec(3, 3, figure=self.fig, hspace=0.35, wspace=0.3,
|
||||||
|
left=0.07, right=0.95, top=0.92, bottom=0.08)
|
||||||
|
plt.show(block=False)
|
||||||
|
|
||||||
|
self.fig.clear()
|
||||||
|
self.fig.suptitle(f'PHANTOM Market Dynamics [t={env._step_count}, a={env.alpha:.2f}]',
|
||||||
|
fontsize=14, fontweight='bold')
|
||||||
|
|
||||||
|
demand_mat = np.array(env._demand_history).T
|
||||||
|
price_mat = np.array(env._price_history).T
|
||||||
|
elasticity = env._compute_elasticity()
|
||||||
|
|
||||||
|
self._render_scatter(env)
|
||||||
|
self._render_elasticity_bar(env, elasticity)
|
||||||
|
self._render_session_pie(env)
|
||||||
|
self._render_price_heatmap(price_mat)
|
||||||
|
self._render_demand_heatmap(demand_mat)
|
||||||
|
self._render_correlation(env.n_products, price_mat, demand_mat)
|
||||||
|
self._render_revenue(env)
|
||||||
|
|
||||||
|
self.fig.canvas.draw_idle()
|
||||||
|
self.fig.canvas.flush_events()
|
||||||
|
|
||||||
|
def _render_scatter(self, env):
|
||||||
|
ax = self.fig.add_subplot(self.gs[0, 0])
|
||||||
|
prices_flat = np.array(env._price_history).flatten()
|
||||||
|
demands_flat = np.array(env._demand_history).flatten()
|
||||||
|
product_ids = np.tile(np.arange(env.n_products), len(env._price_history))
|
||||||
|
ax.scatter(prices_flat, demands_flat, c=product_ids, cmap='plasma', alpha=0.6, s=15, edgecolors='none')
|
||||||
|
if len(prices_flat) > 1:
|
||||||
|
z = np.polyfit(prices_flat, demands_flat, 1)
|
||||||
|
p_line = np.linspace(prices_flat.min(), prices_flat.max(), 50)
|
||||||
|
ax.plot(p_line, np.polyval(z, p_line), '--', lw=1.5, alpha=0.8)
|
||||||
|
style_axis(ax, "Price-Demand Relationship", "Price ($)", "Demand")
|
||||||
|
|
||||||
|
def _render_elasticity_bar(self, env, elasticity):
|
||||||
|
ax = self.fig.add_subplot(self.gs[0, 1])
|
||||||
|
ax.barh(range(env.n_products), elasticity, alpha=0.8)
|
||||||
|
ax.axvline(0, lw=0.8, alpha=0.5)
|
||||||
|
ax.axvline(-1, lw=1, ls='--', alpha=0.5)
|
||||||
|
ax.set_yticks(range(env.n_products))
|
||||||
|
ax.set_yticklabels([f'P{i}' for i in range(env.n_products)], fontsize=7)
|
||||||
|
style_axis(ax, "Price Elasticity", "(dQ/dP)(P/Q)", None)
|
||||||
|
|
||||||
|
def _render_session_pie(self, env):
|
||||||
|
ax = self.fig.add_subplot(self.gs[0, 2])
|
||||||
|
n_h, n_a = env.market.Nhumans, env.market.Nagents
|
||||||
|
wedges, _ = ax.pie([n_h, n_a], startangle=90, wedgeprops={'linewidth': 2, 'edgecolor': 'white'})
|
||||||
|
ax.legend(wedges, [f'H ({n_h})', f'A ({n_a})'], loc='lower center', fontsize=8,
|
||||||
|
frameon=False, bbox_to_anchor=(0.5, -0.05))
|
||||||
|
ax.set_title("Session Mix", fontsize=11, fontweight='bold')
|
||||||
|
|
||||||
|
def _render_price_heatmap(self, price_mat):
|
||||||
|
ax = self.fig.add_subplot(self.gs[1, :2])
|
||||||
|
im = ax.imshow(price_mat, aspect='auto', cmap='viridis', origin='lower')
|
||||||
|
style_axis(ax, "Price Heatmap P(product, t)", "Step", "Product")
|
||||||
|
cbar = self.fig.colorbar(im, ax=ax, fraction=0.03, pad=0.02)
|
||||||
|
cbar.set_label('$', fontsize=8)
|
||||||
|
|
||||||
|
def _render_demand_heatmap(self, demand_mat):
|
||||||
|
ax = self.fig.add_subplot(self.gs[1, 2])
|
||||||
|
im = ax.imshow(demand_mat, aspect='auto', cmap='Blues', origin='lower')
|
||||||
|
style_axis(ax, "Demand Q(product, t)", "Step", None)
|
||||||
|
self.fig.colorbar(im, ax=ax, fraction=0.046, pad=0.02)
|
||||||
|
|
||||||
|
def _render_correlation(self, n_products, price_mat, demand_mat):
|
||||||
|
ax = self.fig.add_subplot(self.gs[2, 0])
|
||||||
|
if price_mat.shape[1] > 2:
|
||||||
|
corr = np.corrcoef(price_mat, demand_mat)[:n_products, n_products:]
|
||||||
|
im = ax.imshow(corr, cmap='RdBu', vmin=-1, vmax=1, aspect='auto')
|
||||||
|
ax.set_xticks(range(n_products))
|
||||||
|
ax.set_yticks(range(n_products))
|
||||||
|
ax.set_xticklabels([f'Q{i}' for i in range(n_products)], fontsize=6)
|
||||||
|
ax.set_yticklabels([f'P{i}' for i in range(n_products)], fontsize=6)
|
||||||
|
self.fig.colorbar(im, ax=ax, fraction=0.046, pad=0.02)
|
||||||
|
style_axis(ax, "Price-Demand Correlation", None, None)
|
||||||
|
|
||||||
|
def _render_revenue(self, env):
|
||||||
|
ax = self.fig.add_subplot(self.gs[2, 1:])
|
||||||
|
n_steps = len(env._revenue_history)
|
||||||
|
demand_std = [np.std(d) for d in env._demand_history]
|
||||||
|
ax.fill_between(range(n_steps), env._revenue_history, alpha=0.3)
|
||||||
|
ax.plot(env._revenue_history, linewidth=2, label='Revenue')
|
||||||
|
ax.set_xlim(0, max(n_steps, 1))
|
||||||
|
ax.set_ylim(0, max(env._revenue_history) * 1.1 if env._revenue_history else 1)
|
||||||
|
|
||||||
|
ax2 = ax.twinx()
|
||||||
|
ax2.plot(range(n_steps), demand_std, linewidth=2, ls='-', alpha=0.9, label='sigma(Demand)')
|
||||||
|
d_min, d_max = min(demand_std), max(demand_std)
|
||||||
|
margin = (d_max - d_min) * 0.2 if d_max > d_min else 0.5
|
||||||
|
ax2.set_ylim(max(0, d_min - margin), d_max + margin)
|
||||||
|
ax2.set_ylabel('Demand sigma', fontsize=9)
|
||||||
|
|
||||||
|
style_axis(ax, "Revenue & Demand Dispersion", "Step", "Revenue ($)")
|
||||||
|
ax.legend(loc='upper left', fontsize=7, frameon=False)
|
||||||
|
ax2.legend(loc='upper right', fontsize=7, frameon=False)
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self.fig:
|
||||||
|
plt.close(self.fig)
|
||||||
|
self.fig = None
|
||||||
77
engine/lib/wrappers.py
Normal file
77
engine/lib/wrappers.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
"""Economic metrics wrapper - calculates thesis-aligned KPIs and injects into info dict."""
|
||||||
|
|
||||||
|
import gymnasium as gym
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
|
||||||
|
class EconomicMetricsWrapper(gym.Wrapper):
|
||||||
|
"""Calculates thesis-aligned economic metrics per step, injects into info.
|
||||||
|
|
||||||
|
Metrics follow thesis definitions:
|
||||||
|
- COI level: E[P] - p_min (Definition 1)
|
||||||
|
- Margin: (avg_price - p_min) / avg_price
|
||||||
|
- Regret: 1 - (revenue / baseline_revenue)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, env: gym.Env, p_min: float = 10.0, baseline_revenue: float | None = None
|
||||||
|
):
|
||||||
|
super().__init__(env)
|
||||||
|
self.p_min = p_min
|
||||||
|
self.baseline_revenue = baseline_revenue
|
||||||
|
self._price_history: list[np.ndarray] = []
|
||||||
|
self._revenue_history: list[float] = []
|
||||||
|
|
||||||
|
def reset(self, **kwargs):
|
||||||
|
obs, info = self.env.reset(**kwargs)
|
||||||
|
self._price_history = []
|
||||||
|
self._revenue_history = []
|
||||||
|
return obs, info
|
||||||
|
|
||||||
|
def step(self, action):
|
||||||
|
obs, reward, terminated, truncated, info = self.env.step(action)
|
||||||
|
|
||||||
|
# extract from unwrapped env
|
||||||
|
prices = self.env.unwrapped._prices
|
||||||
|
demand_dict = self.env.unwrapped._demand
|
||||||
|
demand = np.array([demand_dict.get(i, 0.0) for i in range(len(prices))])
|
||||||
|
alpha = self.env.unwrapped.alpha
|
||||||
|
|
||||||
|
# core calculations
|
||||||
|
revenue = float(np.sum(prices * demand))
|
||||||
|
avg_price = float(np.mean(prices))
|
||||||
|
margin = (avg_price - self.p_min) / max(avg_price, 1e-6)
|
||||||
|
coi_level = avg_price - self.p_min # E[P] - p_min per thesis Def 1
|
||||||
|
|
||||||
|
self._price_history.append(prices.copy())
|
||||||
|
self._revenue_history.append(revenue)
|
||||||
|
|
||||||
|
# regret vs baseline (golden path)
|
||||||
|
regret = 0.0
|
||||||
|
if self.baseline_revenue and self.baseline_revenue > 0:
|
||||||
|
regret = 1.0 - (revenue / self.baseline_revenue)
|
||||||
|
|
||||||
|
# inject structured metrics into info
|
||||||
|
info["economics"] = {
|
||||||
|
"revenue": revenue,
|
||||||
|
"margin": margin,
|
||||||
|
"coi_level": coi_level,
|
||||||
|
"regret": regret,
|
||||||
|
}
|
||||||
|
for key in ("coi_mix", "coi_base", "coi_leakage", "coi_penalty"):
|
||||||
|
if key in info:
|
||||||
|
info["economics"][key] = info[key]
|
||||||
|
info["prices"] = prices.copy()
|
||||||
|
info["demand"] = demand.copy()
|
||||||
|
|
||||||
|
return obs, reward, terminated, truncated, info
|
||||||
|
|
||||||
|
@property
|
||||||
|
def episode_revenue(self) -> float:
|
||||||
|
return sum(self._revenue_history)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def episode_mean_price(self) -> float:
|
||||||
|
if not self._price_history:
|
||||||
|
return 0.0
|
||||||
|
return float(np.mean([np.mean(p) for p in self._price_history]))
|
||||||
34
engine/studies/factors.py
Normal file
34
engine/studies/factors.py
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
"""shared factor definitions for experimental designs"""
|
||||||
|
import numpy as np
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import Callable, Any
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Factor:
|
||||||
|
name: str
|
||||||
|
levels: list
|
||||||
|
primary: bool = True # full cross vs sampled
|
||||||
|
|
||||||
|
# demand functions with compatible signatures
|
||||||
|
def demand_linear(mu, sigma, size): return np.maximum(0, np.random.normal(mu, sigma, size))
|
||||||
|
def demand_uniform(mu, sigma, size): return np.random.uniform(mu - sigma, mu + sigma, size)
|
||||||
|
def demand_exponential(mu, sigma, size): return np.random.exponential(mu, size)
|
||||||
|
def demand_logistic(mu, sigma, size): return np.random.logistic(mu, sigma, size)
|
||||||
|
|
||||||
|
DEMAND_FUNCTIONS = {
|
||||||
|
"linear": demand_linear,
|
||||||
|
"uniform": demand_uniform,
|
||||||
|
"exponential": demand_exponential,
|
||||||
|
"logistic": demand_logistic,
|
||||||
|
}
|
||||||
|
|
||||||
|
FACTORS = [
|
||||||
|
Factor("demand_fn", list(DEMAND_FUNCTIONS.keys()), primary=True),
|
||||||
|
Factor("alpha", [0.1, 0.3, 0.5, 0.7], primary=True),
|
||||||
|
Factor("n_products", [5, 15, 30, 50], primary=True),
|
||||||
|
Factor("demand_mu", [30.0, 50.0, 70.0], primary=False),
|
||||||
|
Factor("demand_sigma", [5.0, 10.0, 20.0], primary=False),
|
||||||
|
Factor("N", [100, 500, 1000], primary=False),
|
||||||
|
]
|
||||||
|
|
||||||
|
SEEDS_PER_CONFIG = 5
|
||||||
89
engine/studies/full_factorial.py
Normal file
89
engine/studies/full_factorial.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
"""full factorial design - all factor combinations"""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, "..")
|
||||||
|
import logging
|
||||||
|
from itertools import product
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
from concurrent.futures import ProcessPoolExecutor
|
||||||
|
from .factors import FACTORS, DEMAND_FUNCTIONS, SEEDS_PER_CONFIG
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
def generate_configs():
|
||||||
|
"""generate all factor combinations with seeds"""
|
||||||
|
all_levels = [f.levels for f in FACTORS]
|
||||||
|
names = [f.name for f in FACTORS]
|
||||||
|
|
||||||
|
configs = []
|
||||||
|
for combo in product(*all_levels):
|
||||||
|
base = {names[i]: combo[i] for i in range(len(names))}
|
||||||
|
for seed in range(SEEDS_PER_CONFIG):
|
||||||
|
cfg = {**base, "seed": seed}
|
||||||
|
cfg["id"] = hashlib.md5(json.dumps(cfg, sort_keys=True).encode()).hexdigest()[:8]
|
||||||
|
configs.append(cfg)
|
||||||
|
return configs
|
||||||
|
|
||||||
|
def run_single(cfg: dict) -> dict:
|
||||||
|
"""execute one experiment config, return metrics"""
|
||||||
|
from engine.wrapper import PHANTOM
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
np.random.seed(cfg["seed"])
|
||||||
|
demand_fn = DEMAND_FUNCTIONS[cfg["demand_fn"]]
|
||||||
|
|
||||||
|
env = PHANTOM(
|
||||||
|
n_products=cfg["n_products"],
|
||||||
|
alpha=cfg["alpha"],
|
||||||
|
N=cfg["N"],
|
||||||
|
)
|
||||||
|
env.market.demand = (demand_fn, (cfg["demand_mu"], cfg["demand_sigma"]))
|
||||||
|
|
||||||
|
obs, _ = env.reset()
|
||||||
|
total_reward, steps = 0.0, 0
|
||||||
|
|
||||||
|
for _ in range(100):
|
||||||
|
action = env.action_space.sample()
|
||||||
|
obs, reward, term, trunc, _ = env.step(action)
|
||||||
|
total_reward += reward
|
||||||
|
steps += 1
|
||||||
|
if term: break
|
||||||
|
|
||||||
|
env.close()
|
||||||
|
return {
|
||||||
|
"id": cfg["id"],
|
||||||
|
"config": cfg,
|
||||||
|
"total_reward": total_reward,
|
||||||
|
"avg_reward": total_reward / steps if steps > 0 else 0.0,
|
||||||
|
"steps": steps,
|
||||||
|
}
|
||||||
|
|
||||||
|
def run_study(max_workers: int = None, output: str = "results_full.jsonl"):
|
||||||
|
configs = generate_configs()
|
||||||
|
log.info(f"full factorial: {len(configs)} configs ({len(configs)//SEEDS_PER_CONFIG} unique × {SEEDS_PER_CONFIG} seeds)")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
with ProcessPoolExecutor(max_workers=max_workers) as ex:
|
||||||
|
for i, result in enumerate(ex.map(run_single, configs)):
|
||||||
|
results.append(result)
|
||||||
|
if (i+1) % 100 == 0: log.info(f"progress: {i+1}/{len(configs)}")
|
||||||
|
|
||||||
|
Path(output).write_text("\n".join(json.dumps(r) for r in results))
|
||||||
|
log.info(f"wrote {len(results)} results to {output}")
|
||||||
|
return results
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument("--workers", type=int, default=None)
|
||||||
|
p.add_argument("--output", default="results_full.jsonl")
|
||||||
|
p.add_argument("--dry-run", action="store_true", help="only show design size")
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
configs = generate_configs()
|
||||||
|
log.info(f"design: {len(configs)} runs | factors: {[f.name for f in FACTORS]} | levels: {[len(f.levels) for f in FACTORS]}")
|
||||||
|
|
||||||
|
if not args.dry_run:
|
||||||
|
run_study(args.workers, args.output)
|
||||||
106
engine/studies/mixed_lh.py
Normal file
106
engine/studies/mixed_lh.py
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
"""mixed design: full factorial on primary factors, latin hypercube on secondary"""
|
||||||
|
import sys
|
||||||
|
sys.path.insert(0, "..")
|
||||||
|
import logging
|
||||||
|
from itertools import product
|
||||||
|
import json
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
from concurrent.futures import ProcessPoolExecutor
|
||||||
|
import numpy as np
|
||||||
|
from scipy.stats.qmc import LatinHypercube
|
||||||
|
from factors import FACTORS, DEMAND_FUNCTIONS, SEEDS_PER_CONFIG
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
LH_SAMPLES = 10
|
||||||
|
|
||||||
|
def generate_configs(lh_samples: int = LH_SAMPLES):
|
||||||
|
primary = [f for f in FACTORS if f.primary]
|
||||||
|
secondary = [f for f in FACTORS if not f.primary]
|
||||||
|
|
||||||
|
primary_grid = list(product(*[f.levels for f in primary]))
|
||||||
|
lhs = LatinHypercube(d=len(secondary), seed=42)
|
||||||
|
|
||||||
|
configs = []
|
||||||
|
for p_combo in primary_grid:
|
||||||
|
samples = lhs.random(n=lh_samples)
|
||||||
|
for s in samples:
|
||||||
|
sec_vals = {
|
||||||
|
secondary[i].name: secondary[i].levels[int(s[i] * len(secondary[i].levels))]
|
||||||
|
for i in range(len(secondary))
|
||||||
|
}
|
||||||
|
base = {primary[i].name: p_combo[i] for i in range(len(primary))}
|
||||||
|
base.update(sec_vals)
|
||||||
|
|
||||||
|
for seed in range(SEEDS_PER_CONFIG):
|
||||||
|
cfg = {**base, "seed": seed}
|
||||||
|
cfg["id"] = hashlib.md5(json.dumps(cfg, sort_keys=True).encode()).hexdigest()[:8]
|
||||||
|
configs.append(cfg)
|
||||||
|
return configs
|
||||||
|
|
||||||
|
def run_single(cfg: dict) -> dict:
|
||||||
|
from engine.wrapper import PHANTOM
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
np.random.seed(cfg["seed"])
|
||||||
|
demand_fn = DEMAND_FUNCTIONS[cfg["demand_fn"]]
|
||||||
|
|
||||||
|
env = PHANTOM(
|
||||||
|
n_products=cfg["n_products"],
|
||||||
|
alpha=cfg["alpha"],
|
||||||
|
N=cfg["N"],
|
||||||
|
)
|
||||||
|
env.market.demand = (demand_fn, (cfg["demand_mu"], cfg["demand_sigma"]))
|
||||||
|
|
||||||
|
obs, _ = env.reset()
|
||||||
|
total_reward, steps = 0.0, 0
|
||||||
|
|
||||||
|
for _ in range(100):
|
||||||
|
action = env.action_space.sample()
|
||||||
|
obs, reward, term, trunc, _ = env.step(action)
|
||||||
|
total_reward += reward
|
||||||
|
steps += 1
|
||||||
|
if term: break
|
||||||
|
|
||||||
|
env.close()
|
||||||
|
return {
|
||||||
|
"id": cfg["id"],
|
||||||
|
"config": cfg,
|
||||||
|
"total_reward": total_reward,
|
||||||
|
"avg_reward": total_reward / steps,
|
||||||
|
"steps": steps,
|
||||||
|
}
|
||||||
|
|
||||||
|
def run_study(max_workers: int = None, output: str = "results_mixed.jsonl", lh_samples: int = LH_SAMPLES):
|
||||||
|
configs = generate_configs(lh_samples)
|
||||||
|
n_primary_cells = int(np.prod([len(f.levels) for f in FACTORS if f.primary]))
|
||||||
|
log.info(f"mixed LH: {len(configs)} configs ({n_primary_cells} primary × {lh_samples} LH × {SEEDS_PER_CONFIG} seeds)")
|
||||||
|
|
||||||
|
results = []
|
||||||
|
with ProcessPoolExecutor(max_workers=max_workers) as ex:
|
||||||
|
for i, result in enumerate(ex.map(run_single, configs)):
|
||||||
|
results.append(result)
|
||||||
|
if (i+1) % 100 == 0: log.info(f"progress: {i+1}/{len(configs)}")
|
||||||
|
|
||||||
|
Path(output).write_text("\n".join(json.dumps(r) for r in results))
|
||||||
|
log.info(f"wrote {len(results)} results to {output}")
|
||||||
|
return results
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import argparse
|
||||||
|
p = argparse.ArgumentParser()
|
||||||
|
p.add_argument("--workers", type=int, default=None)
|
||||||
|
p.add_argument("--output", default="results_mixed.jsonl")
|
||||||
|
p.add_argument("--lh-samples", type=int, default=10)
|
||||||
|
p.add_argument("--dry-run", action="store_true", help="only show design size")
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
primary = [f for f in FACTORS if f.primary]
|
||||||
|
secondary = [f for f in FACTORS if not f.primary]
|
||||||
|
configs = generate_configs(args.lh_samples)
|
||||||
|
log.info(f"design: {len(configs)} runs | primary: {[f.name for f in primary]} | secondary (LH): {[f.name for f in secondary]}")
|
||||||
|
|
||||||
|
if not args.dry_run:
|
||||||
|
run_study(args.workers, args.output, args.lh_samples)
|
||||||
84
engine/sweeps/model_mix.yaml
Normal file
84
engine/sweeps/model_mix.yaml
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
method: random
|
||||||
|
metric:
|
||||||
|
name: sweep/score
|
||||||
|
goal: maximize
|
||||||
|
command:
|
||||||
|
- ${env}
|
||||||
|
- python
|
||||||
|
- -m
|
||||||
|
- engine.train
|
||||||
|
parameters:
|
||||||
|
algo:
|
||||||
|
values: [ppo, a2c, dqn, qtable]
|
||||||
|
total_timesteps:
|
||||||
|
values: [30000, 50000, 80000]
|
||||||
|
seed:
|
||||||
|
values: [13, 42, 77]
|
||||||
|
n_products:
|
||||||
|
values: [8, 10, 12]
|
||||||
|
alpha:
|
||||||
|
distribution: uniform
|
||||||
|
min: 0.1
|
||||||
|
max: 0.6
|
||||||
|
lambda_coi:
|
||||||
|
distribution: uniform
|
||||||
|
min: 0.05
|
||||||
|
max: 0.6
|
||||||
|
robust_radius:
|
||||||
|
distribution: uniform
|
||||||
|
min: 0.0
|
||||||
|
max: 0.3
|
||||||
|
robust_points:
|
||||||
|
values: [3, 5, 7]
|
||||||
|
info_value:
|
||||||
|
distribution: uniform
|
||||||
|
min: 0.5
|
||||||
|
max: 2.0
|
||||||
|
revenue_weight:
|
||||||
|
values: [0.005, 0.01, 0.02]
|
||||||
|
learning_rate:
|
||||||
|
distribution: log_uniform_values
|
||||||
|
min: 1.0e-5
|
||||||
|
max: 1.0e-3
|
||||||
|
gamma:
|
||||||
|
values: [0.97, 0.99, 0.995]
|
||||||
|
buffer_size:
|
||||||
|
values: [20000, 50000, 100000]
|
||||||
|
batch_size:
|
||||||
|
values: [128, 256, 512]
|
||||||
|
tau:
|
||||||
|
values: [0.002, 0.005, 0.01]
|
||||||
|
train_freq:
|
||||||
|
values: [1, 4, 8]
|
||||||
|
learning_starts:
|
||||||
|
values: [500, 1000, 3000]
|
||||||
|
n_steps:
|
||||||
|
values: [512, 1024, 2048]
|
||||||
|
n_epochs:
|
||||||
|
values: [5, 10, 20]
|
||||||
|
gae_lambda:
|
||||||
|
values: [0.9, 0.95, 0.98]
|
||||||
|
clip_range:
|
||||||
|
values: [0.1, 0.2, 0.3]
|
||||||
|
ent_coef:
|
||||||
|
values: [0.0, 0.005, 0.01]
|
||||||
|
target_update_interval:
|
||||||
|
values: [500, 1000, 2000]
|
||||||
|
exploration_fraction:
|
||||||
|
values: [0.1, 0.2, 0.3]
|
||||||
|
exploration_final_eps:
|
||||||
|
values: [0.01, 0.03, 0.05]
|
||||||
|
action_levels:
|
||||||
|
values: [7, 9, 11]
|
||||||
|
action_scale_low:
|
||||||
|
values: [0.75, 0.8, 0.85]
|
||||||
|
action_scale_high:
|
||||||
|
values: [1.15, 1.2, 1.25]
|
||||||
|
q_lr:
|
||||||
|
values: [0.03, 0.05, 0.1, 0.2]
|
||||||
|
eps_start:
|
||||||
|
value: 1.0
|
||||||
|
eps_end:
|
||||||
|
values: [0.02, 0.05, 0.1]
|
||||||
|
eps_decay:
|
||||||
|
values: [0.999, 0.9995, 0.9999]
|
||||||
85
engine/sweeps/models_only.yaml
Normal file
85
engine/sweeps/models_only.yaml
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
method: grid
|
||||||
|
metric:
|
||||||
|
name: sweep/score
|
||||||
|
goal: maximize
|
||||||
|
run_cap: 4
|
||||||
|
command:
|
||||||
|
- ${env}
|
||||||
|
- python
|
||||||
|
- -m
|
||||||
|
- engine.train
|
||||||
|
parameters:
|
||||||
|
algo:
|
||||||
|
values: [ppo, a2c, dqn, qtable]
|
||||||
|
seed:
|
||||||
|
value: 42
|
||||||
|
total_timesteps:
|
||||||
|
value: 12000
|
||||||
|
eval_episodes:
|
||||||
|
value: 3
|
||||||
|
eval_freq:
|
||||||
|
value: 500
|
||||||
|
log_freq:
|
||||||
|
value: 100
|
||||||
|
revenue_weight:
|
||||||
|
value: 0.01
|
||||||
|
n_products:
|
||||||
|
value: 8
|
||||||
|
N:
|
||||||
|
value: 80
|
||||||
|
alpha:
|
||||||
|
value: 0.3
|
||||||
|
lambda_coi:
|
||||||
|
value: 0.2
|
||||||
|
robust_radius:
|
||||||
|
value: 0.0
|
||||||
|
robust_points:
|
||||||
|
value: 1
|
||||||
|
info_value:
|
||||||
|
value: 1.0
|
||||||
|
learning_rate:
|
||||||
|
value: 0.0003
|
||||||
|
gamma:
|
||||||
|
value: 0.99
|
||||||
|
buffer_size:
|
||||||
|
value: 20000
|
||||||
|
batch_size:
|
||||||
|
value: 128
|
||||||
|
tau:
|
||||||
|
value: 0.005
|
||||||
|
train_freq:
|
||||||
|
value: 1
|
||||||
|
learning_starts:
|
||||||
|
value: 500
|
||||||
|
n_steps:
|
||||||
|
value: 512
|
||||||
|
n_epochs:
|
||||||
|
value: 10
|
||||||
|
gae_lambda:
|
||||||
|
value: 0.95
|
||||||
|
clip_range:
|
||||||
|
value: 0.2
|
||||||
|
ent_coef:
|
||||||
|
value: 0.0
|
||||||
|
target_update_interval:
|
||||||
|
value: 500
|
||||||
|
exploration_fraction:
|
||||||
|
value: 0.2
|
||||||
|
exploration_final_eps:
|
||||||
|
value: 0.05
|
||||||
|
action_levels:
|
||||||
|
value: 7
|
||||||
|
action_scale_low:
|
||||||
|
value: 0.9
|
||||||
|
action_scale_high:
|
||||||
|
value: 1.1
|
||||||
|
q_lr:
|
||||||
|
value: 0.1
|
||||||
|
q_bins:
|
||||||
|
value: 6
|
||||||
|
eps_start:
|
||||||
|
value: 1.0
|
||||||
|
eps_end:
|
||||||
|
value: 0.05
|
||||||
|
eps_decay:
|
||||||
|
value: 0.9995
|
||||||
54
engine/sweeps/sac_tune.yaml
Normal file
54
engine/sweeps/sac_tune.yaml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
method: bayes
|
||||||
|
metric:
|
||||||
|
name: sweep/score
|
||||||
|
goal: maximize
|
||||||
|
command:
|
||||||
|
- ${env}
|
||||||
|
- python
|
||||||
|
- -m
|
||||||
|
- engine.train
|
||||||
|
parameters:
|
||||||
|
algo:
|
||||||
|
value: sac
|
||||||
|
total_timesteps:
|
||||||
|
values: [50000, 80000, 120000]
|
||||||
|
seed:
|
||||||
|
values: [13, 42, 77]
|
||||||
|
alpha:
|
||||||
|
distribution: uniform
|
||||||
|
min: 0.15
|
||||||
|
max: 0.55
|
||||||
|
n_products:
|
||||||
|
values: [8, 10, 12]
|
||||||
|
lambda_coi:
|
||||||
|
distribution: uniform
|
||||||
|
min: 0.05
|
||||||
|
max: 0.5
|
||||||
|
robust_radius:
|
||||||
|
distribution: uniform
|
||||||
|
min: 0.05
|
||||||
|
max: 0.3
|
||||||
|
robust_points:
|
||||||
|
values: [3, 5, 7]
|
||||||
|
info_value:
|
||||||
|
distribution: uniform
|
||||||
|
min: 0.5
|
||||||
|
max: 2.0
|
||||||
|
revenue_weight:
|
||||||
|
values: [0.005, 0.01, 0.02]
|
||||||
|
learning_rate:
|
||||||
|
distribution: log_uniform_values
|
||||||
|
min: 3.0e-5
|
||||||
|
max: 1.0e-3
|
||||||
|
gamma:
|
||||||
|
values: [0.98, 0.99, 0.995]
|
||||||
|
buffer_size:
|
||||||
|
values: [50000, 100000, 200000]
|
||||||
|
batch_size:
|
||||||
|
values: [128, 256, 512]
|
||||||
|
tau:
|
||||||
|
values: [0.002, 0.005, 0.01]
|
||||||
|
train_freq:
|
||||||
|
values: [1, 4, 8]
|
||||||
|
learning_starts:
|
||||||
|
values: [1000, 3000, 5000]
|
||||||
86
engine/sweeps/small_arch_compare.yaml
Normal file
86
engine/sweeps/small_arch_compare.yaml
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
method: random
|
||||||
|
metric:
|
||||||
|
name: sweep/score
|
||||||
|
goal: maximize
|
||||||
|
command:
|
||||||
|
- ${env}
|
||||||
|
- python
|
||||||
|
- -m
|
||||||
|
- engine.train
|
||||||
|
parameters:
|
||||||
|
algo:
|
||||||
|
values: [ppo, a2c, dqn, qtable]
|
||||||
|
arch:
|
||||||
|
values: [tiny, small, medium]
|
||||||
|
activation:
|
||||||
|
values: [relu, tanh]
|
||||||
|
total_timesteps:
|
||||||
|
values: [8000, 12000, 20000]
|
||||||
|
seed:
|
||||||
|
values: [13, 42, 77]
|
||||||
|
n_products:
|
||||||
|
values: [6, 8, 10]
|
||||||
|
alpha:
|
||||||
|
distribution: uniform
|
||||||
|
min: 0.1
|
||||||
|
max: 0.5
|
||||||
|
lambda_coi:
|
||||||
|
distribution: uniform
|
||||||
|
min: 0.05
|
||||||
|
max: 0.4
|
||||||
|
robust_radius:
|
||||||
|
values: [0.0, 0.1, 0.2]
|
||||||
|
robust_points:
|
||||||
|
values: [3, 5]
|
||||||
|
info_value:
|
||||||
|
values: [0.75, 1.0, 1.5]
|
||||||
|
revenue_weight:
|
||||||
|
values: [0.005, 0.01, 0.02]
|
||||||
|
learning_rate:
|
||||||
|
distribution: log_uniform_values
|
||||||
|
min: 1.0e-5
|
||||||
|
max: 5.0e-4
|
||||||
|
gamma:
|
||||||
|
values: [0.98, 0.99]
|
||||||
|
buffer_size:
|
||||||
|
values: [10000, 30000, 50000]
|
||||||
|
batch_size:
|
||||||
|
values: [64, 128, 256]
|
||||||
|
tau:
|
||||||
|
values: [0.002, 0.005, 0.01]
|
||||||
|
train_freq:
|
||||||
|
values: [1, 4]
|
||||||
|
learning_starts:
|
||||||
|
values: [500, 1000, 2000]
|
||||||
|
n_steps:
|
||||||
|
values: [256, 512, 1024]
|
||||||
|
n_epochs:
|
||||||
|
values: [5, 10]
|
||||||
|
gae_lambda:
|
||||||
|
values: [0.9, 0.95]
|
||||||
|
clip_range:
|
||||||
|
values: [0.1, 0.2]
|
||||||
|
ent_coef:
|
||||||
|
values: [0.0, 0.005]
|
||||||
|
target_update_interval:
|
||||||
|
values: [500, 1000]
|
||||||
|
exploration_fraction:
|
||||||
|
values: [0.1, 0.2]
|
||||||
|
exploration_final_eps:
|
||||||
|
values: [0.02, 0.05]
|
||||||
|
action_levels:
|
||||||
|
values: [5, 7, 9]
|
||||||
|
action_scale_low:
|
||||||
|
values: [0.85, 0.9]
|
||||||
|
action_scale_high:
|
||||||
|
values: [1.1, 1.15]
|
||||||
|
q_lr:
|
||||||
|
values: [0.05, 0.1, 0.2]
|
||||||
|
q_bins:
|
||||||
|
values: [4, 6, 8]
|
||||||
|
eps_start:
|
||||||
|
value: 1.0
|
||||||
|
eps_end:
|
||||||
|
values: [0.02, 0.05]
|
||||||
|
eps_decay:
|
||||||
|
values: [0.999, 0.9995]
|
||||||
521
engine/train.py
Normal file
521
engine/train.py
Normal file
@@ -0,0 +1,521 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
import numpy as np
|
||||||
|
|
||||||
|
from .wandb_checkpoint import checkpoint_artifact_name, download_latest_checkpoint
|
||||||
|
|
||||||
|
try:
|
||||||
|
import wandb
|
||||||
|
|
||||||
|
HAS_WANDB = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_WANDB = False
|
||||||
|
|
||||||
|
try:
|
||||||
|
from stable_baselines3 import PPO, A2C, DQN
|
||||||
|
from stable_baselines3.common.callbacks import EvalCallback
|
||||||
|
from stable_baselines3.common.monitor import Monitor
|
||||||
|
|
||||||
|
HAS_SB3 = True
|
||||||
|
except ImportError:
|
||||||
|
HAS_SB3 = False
|
||||||
|
|
||||||
|
from .jax import JAX_AVAILABLE
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_CFG = {
|
||||||
|
"project": "phantom-pricing",
|
||||||
|
"algo": "ppo",
|
||||||
|
"seed": 42,
|
||||||
|
"total_timesteps": 50_000,
|
||||||
|
"eval_episodes": 5,
|
||||||
|
"eval_freq": 1_000,
|
||||||
|
"log_freq": 100,
|
||||||
|
"revenue_weight": 0.01,
|
||||||
|
"n_products": 10,
|
||||||
|
"N": 100,
|
||||||
|
"alpha": 0.3,
|
||||||
|
"lambda_coi": 0.2,
|
||||||
|
"robust_radius": 0.15,
|
||||||
|
"robust_points": 5,
|
||||||
|
"info_value": 1.0,
|
||||||
|
"price_low": 10.0,
|
||||||
|
"price_high": 150.0,
|
||||||
|
"action_levels": 9,
|
||||||
|
"action_scale_low": 0.8,
|
||||||
|
"action_scale_high": 1.2,
|
||||||
|
"learning_rate": 3e-4,
|
||||||
|
"gamma": 0.99,
|
||||||
|
"buffer_size": 50_000,
|
||||||
|
"batch_size": 256,
|
||||||
|
"tau": 0.005,
|
||||||
|
"train_freq": 1,
|
||||||
|
"learning_starts": 1_000,
|
||||||
|
"target_update_interval": 1_000,
|
||||||
|
"exploration_fraction": 0.2,
|
||||||
|
"exploration_final_eps": 0.05,
|
||||||
|
"n_steps": 2_048,
|
||||||
|
"n_epochs": 10,
|
||||||
|
"gae_lambda": 0.95,
|
||||||
|
"clip_range": 0.2,
|
||||||
|
"ent_coef": 0.0,
|
||||||
|
"q_lr": 0.1,
|
||||||
|
"eps_start": 1.0,
|
||||||
|
"eps_end": 0.05,
|
||||||
|
"eps_decay": 0.9995,
|
||||||
|
"model_dir": "engine/models",
|
||||||
|
"arch": "small",
|
||||||
|
"activation": "relu",
|
||||||
|
"q_bins": 6,
|
||||||
|
"max_steps": 100,
|
||||||
|
"margin_floor": 0.05,
|
||||||
|
"margin_floor_patience": 5,
|
||||||
|
"use_jax": False,
|
||||||
|
"jax_num_envs": 16,
|
||||||
|
"jax_num_steps": 128,
|
||||||
|
"jax_num_minibatches": 4,
|
||||||
|
"jax_update_epochs": 4,
|
||||||
|
"jax_anneal_lr": True,
|
||||||
|
"checkpoint_interval": 10_000,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _truthy(value: str | bool | None) -> bool:
|
||||||
|
if isinstance(value, bool):
|
||||||
|
return value
|
||||||
|
if value is None:
|
||||||
|
return False
|
||||||
|
return str(value).strip().lower() in {"1", "true", "yes", "on"}
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg(raw: dict | None = None) -> dict:
|
||||||
|
cfg = dict(DEFAULT_CFG)
|
||||||
|
if raw:
|
||||||
|
cfg.update({k: v for k, v in raw.items() if v is not None})
|
||||||
|
cfg["algo"] = str(cfg["algo"]).lower()
|
||||||
|
cfg["use_jax"] = _truthy(cfg.get("use_jax")) or _truthy(
|
||||||
|
os.environ.get("PHANTOM_USE_JAX")
|
||||||
|
)
|
||||||
|
return cfg
|
||||||
|
|
||||||
|
|
||||||
|
def _wandb_cfg_dict() -> dict:
|
||||||
|
return (
|
||||||
|
{k: wandb.config[k] for k in wandb.config.keys()}
|
||||||
|
if HAS_WANDB and wandb.run
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_env(cfg: dict):
|
||||||
|
from gymnasium.wrappers import FlattenObservation
|
||||||
|
|
||||||
|
from .wrapper import PHANTOM
|
||||||
|
from .lib.wrappers import EconomicMetricsWrapper
|
||||||
|
|
||||||
|
env = PHANTOM(
|
||||||
|
n_products=int(cfg["n_products"]),
|
||||||
|
alpha=float(cfg["alpha"]),
|
||||||
|
N=int(cfg["N"]),
|
||||||
|
price_bounds=(float(cfg["price_low"]), float(cfg["price_high"])),
|
||||||
|
lambda_coi=float(cfg["lambda_coi"]),
|
||||||
|
robust_radius=float(cfg["robust_radius"]),
|
||||||
|
robust_points=int(cfg["robust_points"]),
|
||||||
|
info_value=float(cfg["info_value"]),
|
||||||
|
action_levels=int(cfg["action_levels"]),
|
||||||
|
action_scale_low=float(cfg["action_scale_low"]),
|
||||||
|
action_scale_high=float(cfg["action_scale_high"]),
|
||||||
|
max_steps=int(cfg.get("max_steps", 100)),
|
||||||
|
margin_floor=float(cfg.get("margin_floor", 0.05)),
|
||||||
|
margin_floor_patience=int(cfg.get("margin_floor_patience", 5)),
|
||||||
|
render_mode=None,
|
||||||
|
)
|
||||||
|
env = EconomicMetricsWrapper(env)
|
||||||
|
env = FlattenObservation(env)
|
||||||
|
return env
|
||||||
|
|
||||||
|
|
||||||
|
def _net_arch(name) -> list[int]:
|
||||||
|
presets = {
|
||||||
|
"tiny": [32, 32],
|
||||||
|
"small": [64, 64],
|
||||||
|
"medium": [128, 128],
|
||||||
|
"large": [256, 256],
|
||||||
|
}
|
||||||
|
if isinstance(name, (list, tuple)):
|
||||||
|
return [int(v) for v in name]
|
||||||
|
s = str(name).lower().strip()
|
||||||
|
if s in presets:
|
||||||
|
return presets[s]
|
||||||
|
if "x" in s:
|
||||||
|
try:
|
||||||
|
vals = [int(v) for v in s.split("x") if v]
|
||||||
|
return vals if vals else presets["small"]
|
||||||
|
except ValueError:
|
||||||
|
return presets["small"]
|
||||||
|
return presets["small"]
|
||||||
|
|
||||||
|
|
||||||
|
def _activation(name):
|
||||||
|
try:
|
||||||
|
import torch.nn as nn
|
||||||
|
except ImportError:
|
||||||
|
return None
|
||||||
|
return {
|
||||||
|
"relu": nn.ReLU,
|
||||||
|
"tanh": nn.Tanh,
|
||||||
|
"elu": nn.ELU,
|
||||||
|
"leaky_relu": nn.LeakyReLU,
|
||||||
|
}.get(str(name).lower().strip(), nn.ReLU)
|
||||||
|
|
||||||
|
|
||||||
|
def _policy_kwargs(cfg: dict) -> dict:
|
||||||
|
kw = {"net_arch": _net_arch(cfg.get("arch", "small"))}
|
||||||
|
act = _activation(cfg.get("activation", "relu"))
|
||||||
|
if act is not None:
|
||||||
|
kw["activation_fn"] = act
|
||||||
|
return kw
|
||||||
|
|
||||||
|
|
||||||
|
def _action(agent, obs, deterministic: bool = True):
|
||||||
|
out = agent.predict(obs, deterministic=deterministic)
|
||||||
|
a = out[0] if isinstance(out, tuple) else out
|
||||||
|
if isinstance(a, np.ndarray) and a.size == 1:
|
||||||
|
return int(a.reshape(-1)[0])
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
def evaluate(agent, env, episodes: int) -> dict:
|
||||||
|
rewards, revenues = [], []
|
||||||
|
for _ in range(int(episodes)):
|
||||||
|
obs, _ = env.reset()
|
||||||
|
done, ep_r, ep_rev = False, 0.0, 0.0
|
||||||
|
while not done:
|
||||||
|
obs, reward, term, trunc, info = env.step(_action(agent, obs, True))
|
||||||
|
done = term or trunc
|
||||||
|
ep_r += float(reward)
|
||||||
|
ep_rev += float(
|
||||||
|
info.get("economics", {}).get("revenue", info.get("revenue", 0.0))
|
||||||
|
)
|
||||||
|
rewards.append(ep_r)
|
||||||
|
revenues.append(ep_rev)
|
||||||
|
return {
|
||||||
|
"eval/reward": float(np.mean(rewards)),
|
||||||
|
"eval/revenue": float(np.mean(revenues)),
|
||||||
|
"eval/reward_std": float(np.std(rewards)),
|
||||||
|
"eval/revenue_std": float(np.std(revenues)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_model(cfg: dict, env):
|
||||||
|
algo = cfg["algo"]
|
||||||
|
policy_kwargs = _policy_kwargs(cfg)
|
||||||
|
if algo == "sac":
|
||||||
|
raise ValueError("sac is not supported with the discrete core env")
|
||||||
|
if algo == "ppo":
|
||||||
|
return PPO(
|
||||||
|
"MlpPolicy",
|
||||||
|
env,
|
||||||
|
verbose=1,
|
||||||
|
policy_kwargs=policy_kwargs,
|
||||||
|
seed=int(cfg["seed"]),
|
||||||
|
learning_rate=float(cfg["learning_rate"]),
|
||||||
|
n_steps=int(cfg["n_steps"]),
|
||||||
|
batch_size=int(cfg["batch_size"]),
|
||||||
|
n_epochs=int(cfg["n_epochs"]),
|
||||||
|
gamma=float(cfg["gamma"]),
|
||||||
|
gae_lambda=float(cfg["gae_lambda"]),
|
||||||
|
clip_range=float(cfg["clip_range"]),
|
||||||
|
ent_coef=float(cfg["ent_coef"]),
|
||||||
|
)
|
||||||
|
if algo == "a2c":
|
||||||
|
return A2C(
|
||||||
|
"MlpPolicy",
|
||||||
|
env,
|
||||||
|
verbose=1,
|
||||||
|
policy_kwargs=policy_kwargs,
|
||||||
|
seed=int(cfg["seed"]),
|
||||||
|
learning_rate=float(cfg["learning_rate"]),
|
||||||
|
n_steps=max(5, int(cfg["n_steps"]) // 32),
|
||||||
|
gamma=float(cfg["gamma"]),
|
||||||
|
gae_lambda=float(cfg["gae_lambda"]),
|
||||||
|
ent_coef=float(cfg["ent_coef"]),
|
||||||
|
)
|
||||||
|
if algo == "dqn":
|
||||||
|
return DQN(
|
||||||
|
"MlpPolicy",
|
||||||
|
env,
|
||||||
|
verbose=1,
|
||||||
|
policy_kwargs=policy_kwargs,
|
||||||
|
seed=int(cfg["seed"]),
|
||||||
|
learning_rate=float(cfg["learning_rate"]),
|
||||||
|
buffer_size=int(cfg["buffer_size"]),
|
||||||
|
batch_size=int(cfg["batch_size"]),
|
||||||
|
gamma=float(cfg["gamma"]),
|
||||||
|
train_freq=int(cfg["train_freq"]),
|
||||||
|
learning_starts=int(cfg["learning_starts"]),
|
||||||
|
target_update_interval=int(cfg["target_update_interval"]),
|
||||||
|
exploration_fraction=float(cfg["exploration_fraction"]),
|
||||||
|
exploration_final_eps=float(cfg["exploration_final_eps"]),
|
||||||
|
)
|
||||||
|
raise ValueError(f"unsupported algo '{algo}'")
|
||||||
|
|
||||||
|
|
||||||
|
def _sb3_model_cls(algo: str):
|
||||||
|
if algo == "ppo":
|
||||||
|
return PPO
|
||||||
|
if algo == "a2c":
|
||||||
|
return A2C
|
||||||
|
if algo == "dqn":
|
||||||
|
return DQN
|
||||||
|
raise ValueError(f"unsupported algo '{algo}'")
|
||||||
|
|
||||||
|
|
||||||
|
def train_qtable(cfg: dict) -> tuple[EventQTable, dict]:
|
||||||
|
from .lib.discrete import EventQTable
|
||||||
|
|
||||||
|
np.random.seed(int(cfg["seed"]))
|
||||||
|
env = make_env(cfg)
|
||||||
|
eval_env = make_env(cfg)
|
||||||
|
agent = EventQTable(
|
||||||
|
env.action_space.n,
|
||||||
|
int(cfg["n_products"]),
|
||||||
|
(float(cfg["price_low"]), float(cfg["price_high"])),
|
||||||
|
lr=float(cfg["q_lr"]),
|
||||||
|
gamma=float(cfg["gamma"]),
|
||||||
|
n_bins=int(cfg["q_bins"]),
|
||||||
|
)
|
||||||
|
eps = float(cfg["eps_start"])
|
||||||
|
obs, _ = env.reset(seed=int(cfg["seed"]))
|
||||||
|
for t in range(int(cfg["total_timesteps"])):
|
||||||
|
a, s = agent.act(obs, eps)
|
||||||
|
nxt, reward, term, trunc, info = env.step(a)
|
||||||
|
done = term or trunc
|
||||||
|
agent.update(s, a, float(reward), agent.encode(nxt), done)
|
||||||
|
eps = max(float(cfg["eps_end"]), eps * float(cfg["eps_decay"]))
|
||||||
|
if HAS_WANDB and wandb.run and (t + 1) % int(cfg["log_freq"]) == 0:
|
||||||
|
econ = info.get("economics", {})
|
||||||
|
wandb.log(
|
||||||
|
{
|
||||||
|
"train/reward": float(reward),
|
||||||
|
"train/revenue": float(econ.get("revenue", 0.0)),
|
||||||
|
"train/epsilon": float(eps),
|
||||||
|
},
|
||||||
|
step=t + 1,
|
||||||
|
)
|
||||||
|
obs = env.reset()[0] if done else nxt
|
||||||
|
metrics = evaluate(agent, eval_env, int(cfg["eval_episodes"]))
|
||||||
|
metrics["train/global_step"] = int(cfg["total_timesteps"])
|
||||||
|
env.close()
|
||||||
|
eval_env.close()
|
||||||
|
return agent, metrics
|
||||||
|
|
||||||
|
|
||||||
|
def train_sb3(cfg: dict) -> tuple[object, dict]:
|
||||||
|
if not HAS_SB3:
|
||||||
|
raise ImportError("stable-baselines3 is required for SB3 models")
|
||||||
|
from .lib.callbacks import CheckpointArtifactCallback, MetricsCallback
|
||||||
|
|
||||||
|
env = make_env(cfg)
|
||||||
|
eval_env = make_env(cfg)
|
||||||
|
env = Monitor(env)
|
||||||
|
eval_env = Monitor(eval_env)
|
||||||
|
model = build_model(cfg, env)
|
||||||
|
resume_step = 0
|
||||||
|
if HAS_WANDB and wandb.run is not None:
|
||||||
|
sweep_id = getattr(wandb.run, "sweep_id", None)
|
||||||
|
artifact_name = checkpoint_artifact_name(cfg, backend="sb3", sweep_id=sweep_id)
|
||||||
|
checkpoint_file = f"phantom_{cfg['algo']}_checkpoint.zip"
|
||||||
|
restored = download_latest_checkpoint(artifact_name, file_name=checkpoint_file)
|
||||||
|
if restored is not None:
|
||||||
|
checkpoint_path, metadata = restored
|
||||||
|
model = _sb3_model_cls(cfg["algo"]).load(
|
||||||
|
checkpoint_path.as_posix(), env=env
|
||||||
|
)
|
||||||
|
resume_step = int(metadata.get("step", getattr(model, "num_timesteps", 0)))
|
||||||
|
model.num_timesteps = max(
|
||||||
|
int(getattr(model, "num_timesteps", 0)), resume_step
|
||||||
|
)
|
||||||
|
|
||||||
|
cbs = [MetricsCallback(log_histograms=True, log_freq=int(cfg["log_freq"]))]
|
||||||
|
cbs.append(
|
||||||
|
CheckpointArtifactCallback(
|
||||||
|
cfg,
|
||||||
|
interval=int(cfg.get("checkpoint_interval", 10_000)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
cbs.append(
|
||||||
|
EvalCallback(
|
||||||
|
eval_env,
|
||||||
|
eval_freq=int(cfg["eval_freq"]),
|
||||||
|
n_eval_episodes=int(cfg["eval_episodes"]),
|
||||||
|
deterministic=True,
|
||||||
|
verbose=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
target_steps = int(cfg["total_timesteps"])
|
||||||
|
remaining_steps = max(0, target_steps - int(getattr(model, "num_timesteps", 0)))
|
||||||
|
if remaining_steps > 0:
|
||||||
|
model.learn(
|
||||||
|
total_timesteps=remaining_steps,
|
||||||
|
callback=cbs,
|
||||||
|
reset_num_timesteps=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
model_path = Path(cfg["model_dir"])
|
||||||
|
model_path.mkdir(parents=True, exist_ok=True)
|
||||||
|
model.save(str(model_path / f"phantom_{cfg['algo']}"))
|
||||||
|
metrics = evaluate(model, eval_env, int(cfg["eval_episodes"]))
|
||||||
|
metrics["train/global_step"] = int(model.num_timesteps)
|
||||||
|
env.close()
|
||||||
|
eval_env.close()
|
||||||
|
return model, metrics
|
||||||
|
|
||||||
|
|
||||||
|
def train_once(cfg: dict) -> dict:
|
||||||
|
algo = cfg["algo"]
|
||||||
|
if cfg.get("use_jax"):
|
||||||
|
if not JAX_AVAILABLE:
|
||||||
|
raise ImportError(
|
||||||
|
"JAX backend requested but JAX is not installed. "
|
||||||
|
"Install engine/jax/requirements.txt and jax[tpu] for TPU runs."
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
from .jax.train import train_jax
|
||||||
|
except Exception as exc: # pragma: no cover
|
||||||
|
raise ImportError(f"Failed to import JAX trainer: {exc}") from exc
|
||||||
|
_, metrics = train_jax(cfg)
|
||||||
|
elif algo == "qtable":
|
||||||
|
_, metrics = train_qtable(cfg)
|
||||||
|
else:
|
||||||
|
_, metrics = train_sb3(cfg)
|
||||||
|
metrics["sweep/score"] = float(
|
||||||
|
metrics["eval/reward"] + float(cfg["revenue_weight"]) * metrics["eval/revenue"]
|
||||||
|
)
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
|
||||||
|
def run_wandb(
|
||||||
|
project: str, overrides: dict, mode: str = "online", sweep_mode: bool = False
|
||||||
|
) -> dict:
|
||||||
|
if not HAS_WANDB:
|
||||||
|
raise ImportError("wandb is required for sweep runs")
|
||||||
|
init_kwargs = {"mode": mode}
|
||||||
|
if sweep_mode:
|
||||||
|
run = wandb.init(**init_kwargs)
|
||||||
|
else:
|
||||||
|
run = wandb.init(project=project, config=overrides, **init_kwargs)
|
||||||
|
|
||||||
|
try:
|
||||||
|
cfg = _cfg(_wandb_cfg_dict())
|
||||||
|
if sweep_mode:
|
||||||
|
for k, v in overrides.items():
|
||||||
|
if k not in wandb.config:
|
||||||
|
cfg[k] = v
|
||||||
|
|
||||||
|
metrics = train_once(cfg)
|
||||||
|
step = int(metrics.get("train/global_step", cfg["total_timesteps"]))
|
||||||
|
wandb.log(metrics, step=step)
|
||||||
|
for k, v in metrics.items():
|
||||||
|
run.summary[k] = v
|
||||||
|
return metrics
|
||||||
|
finally:
|
||||||
|
if wandb.run is not None:
|
||||||
|
wandb.finish()
|
||||||
|
|
||||||
|
|
||||||
|
def run_local(overrides: dict) -> dict:
|
||||||
|
cfg = _cfg(overrides)
|
||||||
|
metrics = train_once(cfg)
|
||||||
|
print(json.dumps(metrics, indent=2))
|
||||||
|
return metrics
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = argparse.ArgumentParser(description="PHANTOM training and W&B sweeps")
|
||||||
|
p.add_argument("--project", default=DEFAULT_CFG["project"])
|
||||||
|
p.add_argument("--algo", choices=["ppo", "a2c", "dqn", "qtable"])
|
||||||
|
p.add_argument("--total-timesteps", type=int)
|
||||||
|
p.add_argument("--alpha", type=float)
|
||||||
|
p.add_argument("--n-products", type=int)
|
||||||
|
p.add_argument("--lambda-coi", type=float)
|
||||||
|
p.add_argument("--robust-radius", type=float)
|
||||||
|
p.add_argument("--robust-points", type=int)
|
||||||
|
p.add_argument("--learning-rate", type=float)
|
||||||
|
p.add_argument("--gamma", type=float)
|
||||||
|
p.add_argument("--revenue-weight", type=float)
|
||||||
|
p.add_argument("--max-steps", type=int)
|
||||||
|
p.add_argument("--margin-floor", type=float)
|
||||||
|
p.add_argument("--margin-floor-patience", type=int)
|
||||||
|
p.add_argument("--arch", type=str)
|
||||||
|
p.add_argument("--activation", type=str)
|
||||||
|
p.add_argument("--jax", action="store_true")
|
||||||
|
p.add_argument("--jax-num-envs", type=int)
|
||||||
|
p.add_argument("--jax-num-steps", type=int)
|
||||||
|
p.add_argument("--jax-num-minibatches", type=int)
|
||||||
|
p.add_argument("--jax-update-epochs", type=int)
|
||||||
|
p.add_argument("--jax-anneal-lr", type=str)
|
||||||
|
p.add_argument("--checkpoint-interval", type=int)
|
||||||
|
p.add_argument("--sweep-agent", action="store_true")
|
||||||
|
p.add_argument("--sweep-id", type=str)
|
||||||
|
p.add_argument("--count", type=int, default=0)
|
||||||
|
p.add_argument("--offline", action="store_true")
|
||||||
|
p.add_argument("--no-wandb", action="store_true")
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
overrides = {
|
||||||
|
"algo": args.algo,
|
||||||
|
"total_timesteps": args.total_timesteps,
|
||||||
|
"alpha": args.alpha,
|
||||||
|
"n_products": args.n_products,
|
||||||
|
"lambda_coi": args.lambda_coi,
|
||||||
|
"robust_radius": args.robust_radius,
|
||||||
|
"robust_points": args.robust_points,
|
||||||
|
"learning_rate": args.learning_rate,
|
||||||
|
"gamma": args.gamma,
|
||||||
|
"revenue_weight": args.revenue_weight,
|
||||||
|
"max_steps": args.max_steps,
|
||||||
|
"margin_floor": args.margin_floor,
|
||||||
|
"margin_floor_patience": args.margin_floor_patience,
|
||||||
|
"arch": args.arch,
|
||||||
|
"activation": args.activation,
|
||||||
|
"use_jax": args.jax,
|
||||||
|
"jax_num_envs": args.jax_num_envs,
|
||||||
|
"jax_num_steps": args.jax_num_steps,
|
||||||
|
"jax_num_minibatches": args.jax_num_minibatches,
|
||||||
|
"jax_update_epochs": args.jax_update_epochs,
|
||||||
|
"checkpoint_interval": args.checkpoint_interval,
|
||||||
|
"jax_anneal_lr": _truthy(args.jax_anneal_lr)
|
||||||
|
if args.jax_anneal_lr is not None
|
||||||
|
else None,
|
||||||
|
}
|
||||||
|
overrides = {k: v for k, v in overrides.items() if v is not None}
|
||||||
|
|
||||||
|
if args.sweep_agent:
|
||||||
|
if args.no_wandb:
|
||||||
|
raise ValueError("sweep agent requires wandb")
|
||||||
|
if not args.sweep_id:
|
||||||
|
raise ValueError("--sweep-id is required with --sweep-agent")
|
||||||
|
mode = "offline" if args.offline else "online"
|
||||||
|
wandb.agent(
|
||||||
|
args.sweep_id,
|
||||||
|
function=lambda: run_wandb(
|
||||||
|
args.project, overrides, mode=mode, sweep_mode=True
|
||||||
|
),
|
||||||
|
count=args.count if args.count > 0 else None,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if args.no_wandb or not HAS_WANDB:
|
||||||
|
run_local(overrides)
|
||||||
|
return
|
||||||
|
|
||||||
|
run_wandb(args.project, overrides, mode="offline" if args.offline else "online")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
366
engine/wrapper.py
Normal file
366
engine/wrapper.py
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
import gymnasium as gym
|
||||||
|
from gymnasium import spaces
|
||||||
|
import numpy as np
|
||||||
|
from .engine import Limbo, MarketEngine, PricingEngine
|
||||||
|
from .lib.render import DashboardRenderer
|
||||||
|
from .lib.coi import (
|
||||||
|
compute_uplift_coi,
|
||||||
|
extract_purchases,
|
||||||
|
compute_agent_probability,
|
||||||
|
)
|
||||||
|
from .lib.behavior import get_transition_models, trajectory_to_events
|
||||||
|
from .lib.wrappers import EconomicMetricsWrapper
|
||||||
|
|
||||||
|
|
||||||
|
class _ActionPricingEngine(PricingEngine):
|
||||||
|
def __init__(self, n_products: int, price_bounds: tuple):
|
||||||
|
self._prices = np.full(n_products, price_bounds[0], dtype=float)
|
||||||
|
|
||||||
|
def set_prices(self, prices: np.ndarray):
|
||||||
|
self._prices = np.asarray(prices, dtype=float)
|
||||||
|
|
||||||
|
def act(self, _):
|
||||||
|
return self._prices
|
||||||
|
|
||||||
|
|
||||||
|
class PHANTOM(gym.Env):
|
||||||
|
"""Gymnasium wrapper for Limbo pricing-market simulation implementing thesis COI framework
|
||||||
|
|
||||||
|
reward = R(p,d) - λ·COI_leak(p,τ') per thesis Section on DR-RL
|
||||||
|
COI_leak uses behavioral divergence to estimate agent probability f(τ')
|
||||||
|
robust inner step: min over alpha in Wasserstein interval around nominal alpha
|
||||||
|
actions are discrete global price-scale moves
|
||||||
|
"""
|
||||||
|
|
||||||
|
metadata = {"render_modes": ["human", "ansi"]}
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
n_products: int = 10,
|
||||||
|
alpha: float = 0.3,
|
||||||
|
N: int = 100,
|
||||||
|
human_params: tuple = (50.0, 10.0),
|
||||||
|
agent_params: tuple = (45.0, 15.0),
|
||||||
|
noise_std: float = 1.0,
|
||||||
|
price_bounds: tuple = (10.0, 150.0),
|
||||||
|
lambda_coi: float = 0.1,
|
||||||
|
coi_window: int = 10,
|
||||||
|
robust_radius: float = 0.0,
|
||||||
|
robust_points: int = 5,
|
||||||
|
info_value: float = 1.0,
|
||||||
|
action_levels: int = 9,
|
||||||
|
action_scale_low: float = 0.9,
|
||||||
|
action_scale_high: float = 1.1,
|
||||||
|
max_steps: int = 100,
|
||||||
|
margin_floor: float = 0.05,
|
||||||
|
margin_floor_patience: int = 5,
|
||||||
|
render_mode: str = None,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.n_products = n_products
|
||||||
|
self.price_bounds = price_bounds
|
||||||
|
self.lambda_coi = lambda_coi
|
||||||
|
self.coi_window = coi_window
|
||||||
|
self.max_steps = max(1, int(max_steps))
|
||||||
|
self.margin_floor = float(
|
||||||
|
margin_floor
|
||||||
|
) # terminate if avg margin stays below this for patience steps
|
||||||
|
self.margin_floor_patience = max(1, int(margin_floor_patience))
|
||||||
|
self.render_mode = render_mode
|
||||||
|
self.alpha = float(alpha)
|
||||||
|
self.nominal_alpha = float(alpha)
|
||||||
|
self.N = N
|
||||||
|
self.human_params = human_params
|
||||||
|
self.agent_params = agent_params
|
||||||
|
self.robust_radius = max(0.0, float(robust_radius))
|
||||||
|
self.robust_points = max(1, int(robust_points))
|
||||||
|
self.info_value = float(info_value)
|
||||||
|
self.action_levels = max(2, int(action_levels))
|
||||||
|
self._action_scales = np.linspace(
|
||||||
|
float(action_scale_low), float(action_scale_high), self.action_levels
|
||||||
|
)
|
||||||
|
|
||||||
|
self.market = MarketEngine(
|
||||||
|
alpha=alpha,
|
||||||
|
N=N,
|
||||||
|
human_params=human_params,
|
||||||
|
agent_params=agent_params,
|
||||||
|
noise_std=noise_std,
|
||||||
|
)
|
||||||
|
self._platform_stub = _ActionPricingEngine(n_products, price_bounds)
|
||||||
|
self._limbo = Limbo(self._platform_stub, self.market)
|
||||||
|
self._set_market_mix(self.nominal_alpha)
|
||||||
|
|
||||||
|
self.action_space = spaces.Discrete(self.action_levels)
|
||||||
|
self.observation_space = spaces.Dict(
|
||||||
|
{
|
||||||
|
"demand": spaces.Box(
|
||||||
|
low=0.0, high=100.0, shape=(n_products,), dtype=np.float32
|
||||||
|
),
|
||||||
|
"prices": spaces.Box(
|
||||||
|
low=price_bounds[0],
|
||||||
|
high=price_bounds[1],
|
||||||
|
shape=(n_products,),
|
||||||
|
dtype=np.float32,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
self._prices = None
|
||||||
|
self._demand = None
|
||||||
|
self._step_count = 0
|
||||||
|
self._demand_history = []
|
||||||
|
self._price_history = []
|
||||||
|
self._revenue_history = []
|
||||||
|
self._renderer = None
|
||||||
|
self._initial_episode_prices = None
|
||||||
|
self._trajectories = [] # session trajectories for agent prob calculation
|
||||||
|
self.baseline_prices = np.full(self.n_products, self.price_bounds[0])
|
||||||
|
self._low_margin_streak = 0 # consecutive steps below margin_floor
|
||||||
|
|
||||||
|
# load behavioral models for agent probability estimation
|
||||||
|
try:
|
||||||
|
self._human_trans, self._agent_trans = get_transition_models()
|
||||||
|
except Exception:
|
||||||
|
# fallback if behavioral data unavailable
|
||||||
|
self._human_trans, self._agent_trans = None, None
|
||||||
|
|
||||||
|
def _get_obs(self) -> dict:
|
||||||
|
demand_arr = np.array(
|
||||||
|
[self._demand.get(i, 0.0) for i in range(self.n_products)], dtype=np.float32
|
||||||
|
)
|
||||||
|
return {"demand": demand_arr, "prices": self._prices.astype(np.float32)}
|
||||||
|
|
||||||
|
def _set_market_mix(self, alpha: float):
|
||||||
|
alpha = float(np.clip(alpha, 0.0, 1.0))
|
||||||
|
n_agents = int(self.N * alpha)
|
||||||
|
self.alpha = alpha
|
||||||
|
self.market.alpha = alpha
|
||||||
|
self.market.Nagents = n_agents
|
||||||
|
self.market.Nhumans = self.N - n_agents
|
||||||
|
|
||||||
|
def _decode_action(self, action) -> np.ndarray:
|
||||||
|
base = (
|
||||||
|
self._prices
|
||||||
|
if self._prices is not None
|
||||||
|
else np.full(self.n_products, self.price_bounds[0], dtype=float)
|
||||||
|
)
|
||||||
|
if np.isscalar(action):
|
||||||
|
idx = int(np.clip(int(action), 0, self.action_levels - 1))
|
||||||
|
return np.clip(base * self._action_scales[idx], *self.price_bounds)
|
||||||
|
a = np.asarray(action)
|
||||||
|
if a.size == 1:
|
||||||
|
idx = int(np.clip(int(a.reshape(-1)[0]), 0, self.action_levels - 1))
|
||||||
|
return np.clip(base * self._action_scales[idx], *self.price_bounds)
|
||||||
|
return np.clip(a.astype(float), *self.price_bounds)
|
||||||
|
|
||||||
|
def _compute_agent_prob(self, trajectories=None) -> float:
|
||||||
|
trajectories = (
|
||||||
|
self.market.last_trajectories if trajectories is None else trajectories
|
||||||
|
)
|
||||||
|
if not trajectories or self._human_trans is None or self._agent_trans is None:
|
||||||
|
return float(self.market.alpha)
|
||||||
|
probs = []
|
||||||
|
for traj in trajectories:
|
||||||
|
events = trajectory_to_events(traj)
|
||||||
|
if len(events) < 2:
|
||||||
|
continue
|
||||||
|
probs.append(
|
||||||
|
compute_agent_probability(events, self._human_trans, self._agent_trans)
|
||||||
|
)
|
||||||
|
return float(np.mean(probs)) if probs else float(self.market.alpha)
|
||||||
|
|
||||||
|
def _compute_reward(
|
||||||
|
self, prices: np.ndarray, demand: dict, agent_prob: float, trajectories: list
|
||||||
|
) -> tuple[float, dict]:
|
||||||
|
demand_arr = np.array(
|
||||||
|
[demand.get(i, 0.0) for i in range(self.n_products)], dtype=float
|
||||||
|
)
|
||||||
|
revenue = float(np.dot(prices, demand_arr))
|
||||||
|
purchases = extract_purchases(trajectories)
|
||||||
|
coi_mix = compute_uplift_coi(prices, purchases, self.baseline_prices)
|
||||||
|
# multiplicative penalty so COI term scales with revenue magnitude
|
||||||
|
coi_leakage = float(agent_prob * self.info_value)
|
||||||
|
discount = float(np.clip(1.0 - self.lambda_coi * coi_leakage, 0.0, 1.0))
|
||||||
|
coi_penalty = revenue * (1.0 - discount) # absolute penalty in revenue units
|
||||||
|
reward = revenue * discount
|
||||||
|
return reward, {
|
||||||
|
"revenue": revenue,
|
||||||
|
"coi_mix": float(coi_mix),
|
||||||
|
"coi_base": 0.0,
|
||||||
|
"coi_leakage": coi_leakage,
|
||||||
|
"coi_penalty": coi_penalty,
|
||||||
|
"coi_discount": discount,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _alpha_candidates(self) -> np.ndarray:
|
||||||
|
if self.robust_radius <= 0.0 or self.robust_points == 1:
|
||||||
|
return np.array([self.nominal_alpha], dtype=float)
|
||||||
|
lo = max(0.0, self.nominal_alpha - self.robust_radius)
|
||||||
|
hi = min(1.0, self.nominal_alpha + self.robust_radius)
|
||||||
|
return np.linspace(lo, hi, self.robust_points)
|
||||||
|
|
||||||
|
def _select_adversarial_alpha(
|
||||||
|
self, prices: np.ndarray
|
||||||
|
) -> tuple[float, dict, list, float]:
|
||||||
|
"""inner robust step: pick worst-case alpha and return its outcome directly to avoid double-sampling"""
|
||||||
|
candidates = self._alpha_candidates()
|
||||||
|
best_alpha, worst_reward = float(candidates[0]), np.inf
|
||||||
|
best_demand, best_trajectories, best_agent_prob = None, [], 0.0
|
||||||
|
for alpha in candidates:
|
||||||
|
self._set_market_mix(float(alpha))
|
||||||
|
demand = self.market.act(prices)
|
||||||
|
trajectories = list(self.market.last_trajectories)
|
||||||
|
agent_prob = self._compute_agent_prob(trajectories)
|
||||||
|
reward, _ = self._compute_reward(prices, demand, agent_prob, trajectories)
|
||||||
|
if reward < worst_reward:
|
||||||
|
worst_reward = reward
|
||||||
|
best_alpha, best_demand, best_trajectories, best_agent_prob = (
|
||||||
|
float(alpha),
|
||||||
|
demand,
|
||||||
|
trajectories,
|
||||||
|
agent_prob,
|
||||||
|
)
|
||||||
|
return best_alpha, best_demand, best_trajectories, best_agent_prob
|
||||||
|
|
||||||
|
def _record_history(self):
|
||||||
|
demand_arr = np.array(
|
||||||
|
[self._demand.get(i, 0.0) for i in range(self.n_products)]
|
||||||
|
)
|
||||||
|
self._demand_history.append(demand_arr)
|
||||||
|
self._price_history.append(self._prices.copy())
|
||||||
|
self._revenue_history.append(np.sum(self._prices * demand_arr))
|
||||||
|
|
||||||
|
def reset(self, seed=None, options=None):
|
||||||
|
super().reset(seed=seed)
|
||||||
|
self._set_market_mix(self.nominal_alpha)
|
||||||
|
self._limbo.reset()
|
||||||
|
self._prices = np.random.uniform(*self.price_bounds, size=self.n_products)
|
||||||
|
self._platform_stub.set_prices(self._prices)
|
||||||
|
self._limbo.step()
|
||||||
|
self._demand = self._limbo.step()
|
||||||
|
self._initial_episode_prices = self._prices.copy()
|
||||||
|
self._step_count = 0
|
||||||
|
self._low_margin_streak = 0
|
||||||
|
self._demand_history, self._price_history, self._revenue_history = [], [], []
|
||||||
|
self._trajectories = list(getattr(self.market, "last_trajectories", []))
|
||||||
|
self._record_history()
|
||||||
|
return self._get_obs(), {}
|
||||||
|
|
||||||
|
def step(self, action):
|
||||||
|
self._prices = self._decode_action(action)
|
||||||
|
# inner robust step returns worst-case outcome directly, no re-sampling
|
||||||
|
alpha_adv, self._demand, trajectories, agent_prob = (
|
||||||
|
self._select_adversarial_alpha(self._prices)
|
||||||
|
)
|
||||||
|
self._set_market_mix(alpha_adv)
|
||||||
|
self._platform_stub.set_prices(self._prices)
|
||||||
|
self._step_count += 1
|
||||||
|
self._trajectories.extend(trajectories)
|
||||||
|
|
||||||
|
reward, metrics = self._compute_reward(
|
||||||
|
self._prices, self._demand, agent_prob, trajectories
|
||||||
|
)
|
||||||
|
self._record_history()
|
||||||
|
|
||||||
|
# soft early termination when margin collapses for too long
|
||||||
|
avg_margin = float(np.mean(self._prices) - self.price_bounds[0]) / max(
|
||||||
|
float(np.mean(self._prices)), 1e-6
|
||||||
|
)
|
||||||
|
if avg_margin < self.margin_floor:
|
||||||
|
self._low_margin_streak += 1
|
||||||
|
else:
|
||||||
|
self._low_margin_streak = 0
|
||||||
|
margin_collapsed = self._low_margin_streak >= self.margin_floor_patience
|
||||||
|
terminated = self._step_count >= self.max_steps or margin_collapsed
|
||||||
|
|
||||||
|
info = {
|
||||||
|
"step": self._step_count,
|
||||||
|
"agent_prob": agent_prob,
|
||||||
|
"alpha_adv": float(alpha_adv),
|
||||||
|
"wasserstein_radius": float(self.robust_radius),
|
||||||
|
**metrics,
|
||||||
|
"raw_revenue": np.sum(
|
||||||
|
self._prices
|
||||||
|
* np.array([self._demand.get(i, 0.0) for i in range(self.n_products)])
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return self._get_obs(), reward, terminated, False, info
|
||||||
|
|
||||||
|
def _compute_elasticity(self) -> np.ndarray:
|
||||||
|
"""point elasticity: e = (dQ/dP) * (P/Q) via finite differences, clipped to [-5, 5]"""
|
||||||
|
if len(self._price_history) < 2:
|
||||||
|
return np.zeros(self.n_products)
|
||||||
|
p, q = np.array(self._price_history), np.array(self._demand_history)
|
||||||
|
dp, dq = np.diff(p, axis=0), np.diff(q, axis=0)
|
||||||
|
valid = np.abs(dp) > 0.5
|
||||||
|
with np.errstate(divide="ignore", invalid="ignore"):
|
||||||
|
elasticity = np.where(
|
||||||
|
valid, (dq / dp) * (p[:-1] / np.maximum(q[:-1], 1.0)), 0.0
|
||||||
|
)
|
||||||
|
elasticity = np.nan_to_num(np.clip(elasticity, -5.0, 5.0), nan=0.0)
|
||||||
|
return (
|
||||||
|
np.mean(elasticity, axis=0)
|
||||||
|
if len(elasticity) > 0
|
||||||
|
else np.zeros(self.n_products)
|
||||||
|
)
|
||||||
|
|
||||||
|
def render(self):
|
||||||
|
if self.render_mode == "human":
|
||||||
|
if self._renderer is None:
|
||||||
|
self._renderer = DashboardRenderer()
|
||||||
|
self._renderer.render(self)
|
||||||
|
elif self.render_mode == "ansi":
|
||||||
|
return (
|
||||||
|
f"step={self._step_count}, prices={self._prices}, demand={self._demand}"
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
if self._renderer:
|
||||||
|
self._renderer.close()
|
||||||
|
self._renderer = None
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import wandb
|
||||||
|
from .lib import MetricsCallback
|
||||||
|
|
||||||
|
class RandomPolicy:
|
||||||
|
"""Minimal SB3-compatible random policy for baseline testing."""
|
||||||
|
|
||||||
|
def __init__(self, env):
|
||||||
|
self.env = env
|
||||||
|
self.num_timesteps = 0
|
||||||
|
|
||||||
|
def learn(self, total_timesteps, callback=None):
|
||||||
|
callback.model = self
|
||||||
|
callback.num_timesteps = 0
|
||||||
|
callback.locals = {}
|
||||||
|
callback.on_training_start({}, {})
|
||||||
|
|
||||||
|
obs, _ = self.env.reset()
|
||||||
|
for step in range(total_timesteps):
|
||||||
|
action = self.env.action_space.sample()
|
||||||
|
obs, reward, term, trunc, info = self.env.step(action)
|
||||||
|
self.num_timesteps = step + 1
|
||||||
|
callback.num_timesteps = self.num_timesteps
|
||||||
|
callback.locals = {"infos": [info]}
|
||||||
|
callback.on_step()
|
||||||
|
if term or trunc:
|
||||||
|
callback.on_rollout_end()
|
||||||
|
obs, _ = self.env.reset()
|
||||||
|
return self
|
||||||
|
|
||||||
|
def predict(self, obs, **kwargs):
|
||||||
|
return self.env.action_space.sample(), None
|
||||||
|
|
||||||
|
wandb.init(project="phantom-pricing", config={"policy": "random", "alpha": 0.3})
|
||||||
|
env = EconomicMetricsWrapper(PHANTOM(n_products=15, alpha=0.3, render_mode=None))
|
||||||
|
|
||||||
|
model = RandomPolicy(env)
|
||||||
|
model.learn(total_timesteps=1000, callback=MetricsCallback())
|
||||||
|
|
||||||
|
print(f"Episode revenue: {env.episode_revenue:.1f}")
|
||||||
|
wandb.finish()
|
||||||
|
env.close()
|
||||||
@@ -9,6 +9,7 @@ import pandas as pd
|
|||||||
|
|
||||||
from lib.separability import estimate_alpha, load_artifacts, score_session
|
from lib.separability import estimate_alpha, load_artifacts, score_session
|
||||||
|
|
||||||
|
|
||||||
# use relative import when in package context, fallback for standalone
|
# use relative import when in package context, fallback for standalone
|
||||||
try:
|
try:
|
||||||
from sim.rl.behavior_loader.models import AgentBehaviorModel
|
from sim.rl.behavior_loader.models import AgentBehaviorModel
|
||||||
@@ -51,7 +52,6 @@ def _states_to_events(states: list[str]) -> list[SimpleNamespace]:
|
|||||||
)
|
)
|
||||||
return events
|
return events
|
||||||
|
|
||||||
|
|
||||||
def contaminate_dataset(df: pd.DataFrame, on: str = "event_type",
|
def contaminate_dataset(df: pd.DataFrame, on: str = "event_type",
|
||||||
contamination_rate: float = 0.1,
|
contamination_rate: float = 0.1,
|
||||||
agent_data_dir: Path = None) -> pd.DataFrame:
|
agent_data_dir: Path = None) -> pd.DataFrame:
|
||||||
@@ -78,6 +78,7 @@ def contaminate_dataset(df: pd.DataFrame, on: str = "event_type",
|
|||||||
# generate synthetic trajectories
|
# generate synthetic trajectories
|
||||||
new_rows = []
|
new_rows = []
|
||||||
alpha_estimates = []
|
alpha_estimates = []
|
||||||
|
|
||||||
for start_event in start_events:
|
for start_event in start_events:
|
||||||
# sample trajectory from agent model, using a state that contains the event type
|
# sample trajectory from agent model, using a state that contains the event type
|
||||||
mdp_states = model.mdp.get('states', []) if model.mdp else []
|
mdp_states = model.mdp.get('states', []) if model.mdp else []
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from procesing.steps import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
def test_compute_demand(pipeline_context):
|
def test_compute_demand(pipeline_context):
|
||||||
|
random.seed(42) # deterministic test
|
||||||
step = ComputeDemandStep(context=pipeline_context)
|
step = ComputeDemandStep(context=pipeline_context)
|
||||||
|
|
||||||
# Test with normal interaction data
|
# Test with normal interaction data
|
||||||
@@ -26,6 +27,7 @@ def test_compute_demand(pipeline_context):
|
|||||||
|
|
||||||
|
|
||||||
def test_compute_demand_skewed(pipeline_context):
|
def test_compute_demand_skewed(pipeline_context):
|
||||||
|
random.seed(42) # deterministic test
|
||||||
step = ComputeDemandStep(context=pipeline_context)
|
step = ComputeDemandStep(context=pipeline_context)
|
||||||
|
|
||||||
# Test with normal interaction data
|
# Test with normal interaction data
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
# MOS (Money Operating System)
|
|
||||||
|
|
||||||
Research-grade quote-control simulator for studying dynamic pricing and market making policies.
|
|
||||||
The system models pricing as a closed loop of **Quote → Arrival → Execution → Position**, enabling
|
|
||||||
controlled experimentation with demand models, inventory constraints, and reward shaping.
|
|
||||||
|
|
||||||
## Core Loop
|
|
||||||
|
|
||||||
1. **Quote** – the policy posts prices (one-sided or two-sided depending on the mechanism).
|
|
||||||
2. **Arrival** – a population model generates purchase opportunities or market orders.
|
|
||||||
3. **Execution** – an execution model decides whether an arrival converts at the quoted price.
|
|
||||||
4. **Position** – inventory/position limits censor fills and generate holding/shortage costs.
|
|
||||||
5. **Observation & Reward** – censored fills and aggregate metrics are exposed to the agent, while
|
|
||||||
objectives turn metrics into a scalar reward.
|
|
||||||
|
|
||||||
Each stage is pluggable via light-weight protocols so you can swap in alternative mechanisms,
|
|
||||||
demand models, or objectives without rewriting the rest of the simulator.
|
|
||||||
|
|
||||||
## Package Layout
|
|
||||||
|
|
||||||
| Module | Purpose |
|
|
||||||
|-------------------|---------|
|
|
||||||
| `lab.outlet` | Core simulation engine, domain types, pricing mechanisms, objectives. |
|
|
||||||
| `lab.population` | Demand arrival models, execution probability models, competitor/market dynamics. |
|
|
||||||
| `lab.experiments` | Rollout utilities, baseline policies, and off-policy evaluation helpers. |
|
|
||||||
| `lab.config` | Convenience factories for preconfigured retail and market-making environments. |
|
|
||||||
|
|
||||||
## Preconfigured Scenarios
|
|
||||||
|
|
||||||
### Retail Dynamic Pricing
|
|
||||||
- Mechanism: posted prices with margin and delta constraints.
|
|
||||||
- Arrivals: browsing sessions with contamination support (scrapers).
|
|
||||||
- Execution: elasticity model with competitor cross-effects.
|
|
||||||
- Position: inventory tracking with holding and shortage costs.
|
|
||||||
- Market: reactive competitor that can trigger price wars.
|
|
||||||
- Objective: PnL minus volatility, holding cost, and lost opportunity penalties.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from lab.config import make_retail_platform
|
|
||||||
from lab.experiments import rollout, fixed_price_policy
|
|
||||||
|
|
||||||
platform = make_retail_platform()
|
|
||||||
policy = fixed_price_policy(platform.instruments.refs)
|
|
||||||
result = rollout(platform, policy, n_steps=100)
|
|
||||||
print(result.total_pnl)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Market Making
|
|
||||||
- Mechanism: two-sided quoting with bid/ask spreads.
|
|
||||||
- Arrivals: Hawkes order flow for clustered demand.
|
|
||||||
- Execution: Avellaneda–Stoikov style intensity model.
|
|
||||||
- Position: inventory risk limits and quadratic penalty objective.
|
|
||||||
- Market: geometric Brownian motion mid-price process.
|
|
||||||
- Objective: PnL plus spread capture minus inventory risk.
|
|
||||||
|
|
||||||
```python
|
|
||||||
from lab.config import make_market_making_platform
|
|
||||||
from lab.experiments import rollout
|
|
||||||
|
|
||||||
platform = make_market_making_platform()
|
|
||||||
mm_policy = lambda obs, t: (platform.instruments.refs, 1.0)
|
|
||||||
result = rollout(platform, mm_policy, n_steps=200, seed=42)
|
|
||||||
print(result.total_pnl)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Extending the Simulator
|
|
||||||
|
|
||||||
- Implement `lab.outlet.protocols.Mechanism` or `ArrivalModel` to introduce new pricing
|
|
||||||
domains or demand processes.
|
|
||||||
- Compose objectives with `lab.outlet.objectives.factory.make_composite` to study alternate
|
|
||||||
reward formulations.
|
|
||||||
- Use `lab.experiments.compare_policies` to benchmark candidate policies across multiple
|
|
||||||
random seeds.
|
|
||||||
|
|
||||||
Comprehensive API documentation lives in `lab/docs` (build with `make html`).
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
"""
|
|
||||||
Quote-Control Simulator: Research-grade platform for dynamic pricing and market making
|
|
||||||
|
|
||||||
The platform abstracts pricing as: Quote -> Arrival -> Execution -> Position
|
|
||||||
Supports multiple mechanisms:
|
|
||||||
- PostedPrice: retail dynamic pricing
|
|
||||||
- TwoSided: market making with bid-ask spreads
|
|
||||||
- Auction: reserve/shading for auction settings
|
|
||||||
|
|
||||||
Example usage:
|
|
||||||
from lab.config import make_retail_platform
|
|
||||||
from lab.experiments import rollout, fixed_price_policy
|
|
||||||
|
|
||||||
platform = make_retail_platform()
|
|
||||||
policy = fixed_price_policy(platform.instruments.refs)
|
|
||||||
result = rollout(platform, policy, n_steps=100)
|
|
||||||
print(f"Total PnL: {result.total_pnl:.2f}")
|
|
||||||
"""
|
|
||||||
|
|
||||||
from .config import make_retail_platform, make_market_making_platform, RetailConfig, MarketMakingConfig
|
|
||||||
from .outlet import Platform, PlatformConfig, Quote, Observation, StepResult
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'make_retail_platform', 'make_market_making_platform',
|
|
||||||
'RetailConfig', 'MarketMakingConfig',
|
|
||||||
'Platform', 'PlatformConfig', 'Quote', 'Observation', 'StepResult',
|
|
||||||
]
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
"""
|
|
||||||
Case studies implementing specific research scenarios.
|
|
||||||
|
|
||||||
Available cases:
|
|
||||||
- thesis: PHANTOM thesis implementation with contaminated demand and DR-RL
|
|
||||||
"""
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
"""
|
|
||||||
Thesis-specific implementation of the PHANTOM pricing defense framework.
|
|
||||||
|
|
||||||
This module implements the mathematical models from the thesis:
|
|
||||||
- ContaminatedArrivalModel: Mixture demand Q(p) = (1-α)d_H + αd_A (Eq 3)
|
|
||||||
- HybridExecutionModel: Divergent H/A behavior with separability (Section 2.1)
|
|
||||||
- RobustStackelbergObjective: Maximin objective with COI penalty (Eq 23)
|
|
||||||
- COIMetrics: Cost of Information tracking (Definition 1)
|
|
||||||
|
|
||||||
The platform configuration creates a research environment that directly
|
|
||||||
maps to the thesis mathematical framework for DR-RL experiments.
|
|
||||||
"""
|
|
||||||
from .arrivals import ContaminatedArrivalModel, ContaminatedArrivalConfig
|
|
||||||
from .execution import HybridExecutionModel, HybridExecutionConfig
|
|
||||||
from .objectives import RobustStackelbergObjective, COIObjective
|
|
||||||
from .platform import make_thesis_platform, ThesisConfig
|
|
||||||
from .metrics import COIMetrics, compute_coi, compute_separability
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'ContaminatedArrivalModel', 'ContaminatedArrivalConfig',
|
|
||||||
'HybridExecutionModel', 'HybridExecutionConfig',
|
|
||||||
'RobustStackelbergObjective', 'COIObjective',
|
|
||||||
'make_thesis_platform', 'ThesisConfig',
|
|
||||||
'COIMetrics', 'compute_coi', 'compute_separability',
|
|
||||||
]
|
|
||||||
@@ -1,327 +0,0 @@
|
|||||||
"""Contaminated arrivals using learned MDP kernels from behavior_loader.
|
|
||||||
|
|
||||||
Implements thesis demand model (Section 3.1):
|
|
||||||
- Aggregate demand Q(p) = (1-α)E[d(p;θ_H)] + αE[d(p;θ_A)] + ε_t (Eq 3)
|
|
||||||
- Demand proxy q̂_{t,i} = Σ_s Σ_k ω(a_{s,k}) · 1[i_{s,k} = i] (Eq 2)
|
|
||||||
- Per-session separability via KL divergence Δ_H, Δ_A (Eq 20-21)
|
|
||||||
|
|
||||||
The arrival model samples sessions from a mixture of human/agent behavioral profiles,
|
|
||||||
each session produces a trajectory τ_s and associated demand computation q(τ').
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from types import SimpleNamespace
|
|
||||||
from typing import Dict, List, Tuple, Optional
|
|
||||||
import numpy as np
|
|
||||||
from ...outlet.types import Opportunity, InstrumentSet, MarketState, HiddenState
|
|
||||||
from ...outlet.constants import Side, OpportunityType
|
|
||||||
from ...outlet.math_util import poisson_arrivals
|
|
||||||
|
|
||||||
try:
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
|
||||||
from sim.rl.behavior_loader.models import (
|
|
||||||
BehaviorModel, AgentBehaviorModel, aggregate_event_transitions, kl_divergence
|
|
||||||
)
|
|
||||||
REAL_MDP = True
|
|
||||||
except ImportError:
|
|
||||||
REAL_MDP = False
|
|
||||||
kl_divergence = None
|
|
||||||
|
|
||||||
EVENT_PAGE = {"session_start": "/", "view_item_page": "/products", "learn_more_about_item": "/products/details",
|
|
||||||
"add_item_to_cart": "/cart", "purchase_complete": "/checkout", "session_end": "/checkout/success"}
|
|
||||||
EVENT_CANON = {"page_view": "session_start", "hover_over_paragraph": "view_item_page", "hover_over_title": "view_item_page",
|
|
||||||
"view_item_page": "view_item_page", "learn_more_about_item": "learn_more_about_item",
|
|
||||||
"add_item_to_cart": "add_item_to_cart", "checkout_start": "purchase_complete", "remove_item": "view_item_page"}
|
|
||||||
|
|
||||||
# action space partition A = A_nav ∪ A_cart ∪ A_filter ∪ A_dwell with signal weights ω (Table 1)
|
|
||||||
ACTION_WEIGHTS: Dict[str, float] = {
|
|
||||||
"add_item_to_cart": 0.8, "remove_item": 0.6, "checkout_start": 0.9, "purchase_complete": 1.0, # A_cart
|
|
||||||
"hover_over_title": 0.3, "hover_over_paragraph": 0.35, "hover_over_link": 0.25, # A_dwell
|
|
||||||
"page_view": 0.1, "session_start": 0.05, "view_item_page": 0.15, "learn_more_about_item": 0.2, # A_nav
|
|
||||||
"search": 0.05, "filter_date": 0.05, "filter_price": 0.08, "sort": 0.03, "session_end": 0.0, # A_filter
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SessionDemand:
|
|
||||||
"""Per-session demand computation per thesis formulation (Section 3.1).
|
|
||||||
|
|
||||||
Each session s ∈ S produces trajectory τ_s and demand proxy q̂. The platform uses
|
|
||||||
divergence signals Δ_H, Δ_A to estimate per-session contamination α̂(τ').
|
|
||||||
"""
|
|
||||||
session_id: str
|
|
||||||
q: Dict[int, float] # q̂_i demand proxy per product (Eq 2)
|
|
||||||
trajectory: List[Dict] # τ_s = (e_{s,1}, ..., e_{s,L_s})
|
|
||||||
delta_h: float = 0.0 # D_KL(T̂' || T̄_H) (Eq 20)
|
|
||||||
delta_a: float = 0.0 # D_KL(T̂' || T̄_A) (Eq 21)
|
|
||||||
alpha_hat: float = 0.0 # per-session contamination estimate
|
|
||||||
actor_class: str = "H" # ground truth Y_s ∈ {H, A}
|
|
||||||
theta: Dict[str, float] = field(default_factory=dict)
|
|
||||||
|
|
||||||
|
|
||||||
def compute_demand_proxy(events: List[Dict], n_products: int) -> Dict[int, float]:
|
|
||||||
"""Compute q̂_{t,i} = Σ_k ω(a_{s,k}) · 1[i_{s,k} = i] per Eq 2."""
|
|
||||||
q = {i: 0.0 for i in range(n_products)}
|
|
||||||
for e in events:
|
|
||||||
action, pidx = e.get("eventName", ""), e.get("product_idx")
|
|
||||||
if pidx is not None and 0 <= pidx < n_products:
|
|
||||||
q[pidx] += ACTION_WEIGHTS.get(action, 0.1)
|
|
||||||
return q
|
|
||||||
|
|
||||||
|
|
||||||
def compute_session_divergence(events: List[Dict], ref_h: Dict, ref_a: Dict) -> Tuple[float, float]:
|
|
||||||
"""Compute Δ_H, Δ_A divergence signals from trajectory (Eq 20-21)."""
|
|
||||||
if not events or kl_divergence is None:
|
|
||||||
return 0.0, 0.0
|
|
||||||
# build empirical transition kernel from trajectory
|
|
||||||
trans: Dict[str, Dict[str, int]] = {}
|
|
||||||
prev = "session_start"
|
|
||||||
for e in events:
|
|
||||||
curr = e.get("eventName", "session_end")
|
|
||||||
trans.setdefault(prev, {})
|
|
||||||
trans[prev][curr] = trans[prev].get(curr, 0) + 1
|
|
||||||
prev = curr
|
|
||||||
# normalize to probabilities
|
|
||||||
kernel = {}
|
|
||||||
for s, dests in trans.items():
|
|
||||||
total = sum(dests.values())
|
|
||||||
kernel[s] = {d: c / total for d, c in dests.items()} if total > 0 else {}
|
|
||||||
# aggregate to event-level and compute KL divergence against reference kernels
|
|
||||||
delta_h = sum(kl_divergence(kernel.get(s, {}), ref_h.get(s, {})) for s in kernel) / max(len(kernel), 1)
|
|
||||||
delta_a = sum(kl_divergence(kernel.get(s, {}), ref_a.get(s, {})) for s in kernel) / max(len(kernel), 1)
|
|
||||||
return delta_h, delta_a
|
|
||||||
|
|
||||||
def _canonicalize(raw: Dict) -> Dict:
|
|
||||||
out = {}
|
|
||||||
for src, dsts in raw.items():
|
|
||||||
sc = EVENT_CANON.get(src, src)
|
|
||||||
out.setdefault(sc, {})
|
|
||||||
for dst, p in dsts.items():
|
|
||||||
dc = EVENT_CANON.get(dst, dst)
|
|
||||||
out[sc][dc] = out[sc].get(dc, 0.0) + p
|
|
||||||
return {s: {k: v/sum(d.values()) for k, v in d.items()} for s, d in out.items() if sum(d.values()) > 0}
|
|
||||||
|
|
||||||
|
|
||||||
class BehavioralProfile:
|
|
||||||
"""Markov profile from learned MDP kernels (Section 3.5.2).
|
|
||||||
|
|
||||||
Transition kernel T̂_Y estimated via MLE: P̂(s'|s) = N(s,s') / Σ_k N(s,k) (Eq 19)
|
|
||||||
"""
|
|
||||||
STATES = ["session_start", "view_item_page", "learn_more_about_item", "add_item_to_cart", "purchase_complete", "session_end"]
|
|
||||||
# fallback kernels T̄_H, T̄_A when real data unavailable
|
|
||||||
FALLBACK_H = {"session_start": {"view_item_page": 0.85, "session_end": 0.15},
|
|
||||||
"view_item_page": {"learn_more_about_item": 0.4, "add_item_to_cart": 0.3, "view_item_page": 0.2, "session_end": 0.1},
|
|
||||||
"learn_more_about_item": {"add_item_to_cart": 0.5, "view_item_page": 0.3, "session_end": 0.2},
|
|
||||||
"add_item_to_cart": {"purchase_complete": 0.6, "view_item_page": 0.25, "session_end": 0.15},
|
|
||||||
"purchase_complete": {"session_end": 1.0}}
|
|
||||||
FALLBACK_A = {"session_start": {"view_item_page": 0.95, "session_end": 0.05},
|
|
||||||
"view_item_page": {"learn_more_about_item": 0.6, "view_item_page": 0.25, "add_item_to_cart": 0.1, "session_end": 0.05},
|
|
||||||
"learn_more_about_item": {"view_item_page": 0.5, "add_item_to_cart": 0.15, "learn_more_about_item": 0.3, "session_end": 0.05},
|
|
||||||
"add_item_to_cart": {"view_item_page": 0.4, "purchase_complete": 0.2, "session_end": 0.4},
|
|
||||||
"purchase_complete": {"session_end": 1.0}}
|
|
||||||
|
|
||||||
def __init__(self, actor: str, pprobs: np.ndarray, data_dir: str = ""):
|
|
||||||
self.actor, self.pprobs = actor, np.clip(pprobs, 0.0, 0.95)
|
|
||||||
self.trans = self._load(data_dir) # T̂_Y transition kernel
|
|
||||||
self._ensure_terminal()
|
|
||||||
self.dwell = {s: (1.2, 0.5) if actor == "agents" else (2.0, 1.2) for s in self.STATES}
|
|
||||||
|
|
||||||
def _load(self, data_dir: str) -> Dict:
|
|
||||||
if not REAL_MDP or not data_dir:
|
|
||||||
print("using fallback")
|
|
||||||
return dict(self.FALLBACK_A if self.actor == "agents" else self.FALLBACK_H)
|
|
||||||
try:
|
|
||||||
mdp = (AgentBehaviorModel if self.actor == "agents" else BehaviorModel)(data_dir).build_MDP()
|
|
||||||
raw = aggregate_event_transitions(mdp) if mdp.get("transitions") else {}
|
|
||||||
return _canonicalize(raw) if raw else dict(self.FALLBACK_A if self.actor == "agents" else self.FALLBACK_H)
|
|
||||||
except Exception:
|
|
||||||
print("using fallback")
|
|
||||||
return dict(self.FALLBACK_A if self.actor == "agents" else self.FALLBACK_H)
|
|
||||||
|
|
||||||
def _ensure_terminal(self):
|
|
||||||
self.trans.setdefault("purchase_complete", {})["session_end"] = self.trans.get("purchase_complete", {}).get("session_end", 1.0)
|
|
||||||
self.trans.setdefault("session_start", {"view_item_page": 0.7, "learn_more_about_item": 0.2, "session_end": 0.1})
|
|
||||||
|
|
||||||
def _tprobs(self, state: str, pidx: int) -> Dict[str, float]:
|
|
||||||
probs = dict(self.trans.get(state, {"session_end": 1.0}))
|
|
||||||
if state == "add_item_to_cart":
|
|
||||||
base = probs.get("purchase_complete", 0.0)
|
|
||||||
df = float(self.pprobs[pidx]) * (0.3 if self.actor == "agents" else 1.0)
|
|
||||||
adj = np.clip(base * 0.5 + df * 0.5, 0.0, 0.95)
|
|
||||||
rem = max(1e-6, 1.0 - adj)
|
|
||||||
other = sum(v for k, v in probs.items() if k != "purchase_complete")
|
|
||||||
probs = {k: (adj if k == "purchase_complete" else v * rem / max(other, 1e-6)) for k, v in probs.items()}
|
|
||||||
total = sum(probs.values())
|
|
||||||
return {k: v/total for k, v in probs.items()} if total > 0 else {"session_end": 1.0}
|
|
||||||
|
|
||||||
def sample(self, rng: np.random.Generator, sid: str, prices: np.ndarray, costs: np.ndarray) -> Tuple[List[Dict], List[SimpleNamespace]]:
|
|
||||||
events, fevts = [], []
|
|
||||||
state, t, pidx = "session_start", 0.0, int(rng.integers(0, len(prices)))
|
|
||||||
cost, cprice = float(costs[pidx]), max(float(prices[pidx]), float(costs[pidx]) * 1.05)
|
|
||||||
|
|
||||||
while state != "session_end" and len(events) < 40:
|
|
||||||
if state != "session_start":
|
|
||||||
row = {"session_id": sid, "actor": "agent" if self.actor == "agents" else "human",
|
|
||||||
"eventName": state, "product_idx": pidx, "productId": f"product-{pidx:04d}",
|
|
||||||
"price_offered": cprice, "price_paid": 0.0, "page": EVENT_PAGE.get(state, "/"),
|
|
||||||
"ts": t, "unit_cost": cost, "base_price": float(prices[pidx])}
|
|
||||||
if state == "purchase_complete":
|
|
||||||
row["price_paid"] = max(cprice * (1.0 + rng.normal(0.0, 0.015)), cost)
|
|
||||||
events.append(row)
|
|
||||||
fevts.append(SimpleNamespace(eventName=state, page=row["page"], productId=row["productId"], ts=t))
|
|
||||||
|
|
||||||
probs = self._tprobs(state, pidx)
|
|
||||||
state = rng.choice(list(probs.keys()), p=list(probs.values()))
|
|
||||||
sh, sc = self.dwell.get(state, (2.0, 1.0))
|
|
||||||
t += max(0.3, rng.gamma(shape=sh, scale=sc))
|
|
||||||
return events, fevts
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ContaminatedArrivalConfig:
|
|
||||||
base_rate: float = 20.0
|
|
||||||
alpha_contamination: float = 0.2
|
|
||||||
alpha_drift: float = 0.0
|
|
||||||
alpha_bounds: tuple[float, float] = (0.0, 0.5)
|
|
||||||
human_views_range: tuple[int, int] = (1, 4)
|
|
||||||
agent_views_range: tuple[int, int] = (3, 10)
|
|
||||||
agent_systematic: bool = True
|
|
||||||
use_real_behavior: bool = True
|
|
||||||
human_data_dir: str = ""
|
|
||||||
agent_data_dir: str = ""
|
|
||||||
|
|
||||||
|
|
||||||
class ContaminatedArrivalModel:
|
|
||||||
"""Mixture model Q(p) = (1-α)E[d(p;θ_H)] + αE[d(p;θ_A)] + ε_t (Eq 3).
|
|
||||||
|
|
||||||
Samples sessions from human/agent behavioral profiles, computes per-session
|
|
||||||
demand proxy q̂ and divergence signals Δ_H, Δ_A for separability.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cfg: ContaminatedArrivalConfig | None = None):
|
|
||||||
self.cfg = cfg or ContaminatedArrivalConfig()
|
|
||||||
self._alpha = self.cfg.alpha_contamination
|
|
||||||
self._scount = 0
|
|
||||||
self._profiles: Dict[str, BehavioralProfile] = {}
|
|
||||||
self._ref_kernels: Dict[str, Dict] = {} # T̄_H, T̄_A reference kernels
|
|
||||||
self._session_demands: List[SessionDemand] = [] # collected session demands
|
|
||||||
|
|
||||||
@property
|
|
||||||
def alpha(self) -> float:
|
|
||||||
return self._alpha
|
|
||||||
|
|
||||||
def _profile(self, actor: str, pprobs: np.ndarray) -> BehavioralProfile:
|
|
||||||
key = actor
|
|
||||||
if key not in self._profiles:
|
|
||||||
ddir = self.cfg.agent_data_dir if actor == "agents" else self.cfg.human_data_dir
|
|
||||||
if not ddir and self.cfg.use_real_behavior:
|
|
||||||
base = Path(__file__).parent.parent.parent.parent / "experiments"
|
|
||||||
ddir = str(base / ("agents/collected_data" if actor == "agents" else "collected_data"))
|
|
||||||
profile = BehavioralProfile(actor, pprobs, ddir if self.cfg.use_real_behavior else "")
|
|
||||||
self._profiles[key] = profile
|
|
||||||
self._ref_kernels[key] = profile.trans # cache T̄_Y for divergence
|
|
||||||
return self._profiles[key]
|
|
||||||
|
|
||||||
def get_ref_kernels(self) -> Tuple[Dict, Dict]:
|
|
||||||
"""Return reference transition kernels T̄_H, T̄_A for divergence computation."""
|
|
||||||
return (self._ref_kernels.get("humans", BehavioralProfile.FALLBACK_H),
|
|
||||||
self._ref_kernels.get("agents", BehavioralProfile.FALLBACK_A))
|
|
||||||
|
|
||||||
def get_session_demands(self) -> List[SessionDemand]:
|
|
||||||
"""Return collected session demands for downstream analysis."""
|
|
||||||
return self._session_demands
|
|
||||||
|
|
||||||
def sample(self, t: float, dt: float, instruments: InstrumentSet,
|
|
||||||
market: MarketState | None, hidden: HiddenState, rng: np.random.Generator) -> list[Opportunity]:
|
|
||||||
"""Sample arrivals as per Eq 3: mixture of human/agent demand distributions.
|
|
||||||
|
|
||||||
For each session s, computes:
|
|
||||||
- Trajectory τ_s from behavioral profile sampling
|
|
||||||
- Demand proxy q̂ via weighted action aggregation (Eq 2)
|
|
||||||
- Divergence signals Δ_H, Δ_A for separability (Eq 20-21)
|
|
||||||
- Per-session contamination estimate α̂(τ')
|
|
||||||
"""
|
|
||||||
cfg = self.cfg
|
|
||||||
if cfg.alpha_drift != 0:
|
|
||||||
self._alpha = np.clip(self._alpha + cfg.alpha_drift * rng.normal(), *cfg.alpha_bounds)
|
|
||||||
hidden.contamination = self._alpha
|
|
||||||
|
|
||||||
n_sess = poisson_arrivals(cfg.base_rate * hidden.true_demand_intensity, dt, rng)
|
|
||||||
prices, costs = instruments.refs, instruments.costs
|
|
||||||
margin = np.clip((prices - costs) / np.maximum(costs, 1e-3), -0.9, 2.0)
|
|
||||||
hprob, aprob = 0.08 * np.exp(-1.2 * margin), 0.05 * np.exp(-0.6 * margin)
|
|
||||||
ref_h, ref_a = self.get_ref_kernels()
|
|
||||||
|
|
||||||
opps = []
|
|
||||||
for _ in range(n_sess):
|
|
||||||
self._scount += 1
|
|
||||||
sid = f"s{self._scount:06d}"
|
|
||||||
is_agent = rng.random() < self._alpha
|
|
||||||
actor, probs = ("agents", aprob) if is_agent else ("humans", hprob)
|
|
||||||
profile = self._profile(actor, probs)
|
|
||||||
events, fevts = profile.sample(rng, sid, prices, costs)
|
|
||||||
|
|
||||||
# compute demand proxy q̂ per Eq 2
|
|
||||||
q = compute_demand_proxy(events, instruments.n)
|
|
||||||
|
|
||||||
# compute divergence signals Δ_H, Δ_A per Eq 20-21
|
|
||||||
delta_h, delta_a = compute_session_divergence(events, ref_h, ref_a)
|
|
||||||
# per-session contamination estimate α̂(τ') = σ(β(Δ_H - Δ_A))
|
|
||||||
alpha_hat = 1.0 / (1.0 + np.exp(-2.0 * (delta_h - delta_a))) if (delta_h + delta_a) > 0 else 0.5
|
|
||||||
|
|
||||||
theta = ({'price_sensitivity': rng.uniform(0.05, 0.2), 'base_conversion': 0.01, 'info_value': 1.0} if is_agent
|
|
||||||
else {'price_sensitivity': rng.uniform(1.5, 4.0), 'base_conversion': rng.uniform(0.2, 0.5), 'info_value': 0.0})
|
|
||||||
|
|
||||||
# store session demand for downstream analysis
|
|
||||||
self._session_demands.append(SessionDemand(
|
|
||||||
session_id=sid, q=q, trajectory=events, delta_h=delta_h, delta_a=delta_a,
|
|
||||||
alpha_hat=alpha_hat, actor_class="A" if is_agent else "H", theta=theta))
|
|
||||||
|
|
||||||
viewed = list({e["product_idx"] for e in events if "product_idx" in e})
|
|
||||||
if not viewed:
|
|
||||||
vr = cfg.agent_views_range if is_agent else cfg.human_views_range
|
|
||||||
viewed = list(rng.choice(instruments.n, size=min(rng.integers(*vr), instruments.n), replace=False))
|
|
||||||
|
|
||||||
for vi, iid in enumerate(viewed):
|
|
||||||
opps.append(Opportunity(
|
|
||||||
id=f"{sid}-{iid}", type=OpportunityType.SESSION, side=Side.BUY,
|
|
||||||
instrument_id=int(iid), size=1.0, t=t + rng.uniform(0, dt),
|
|
||||||
context={'session_id': sid, 'actor_class': 'AGENT' if is_agent else 'HUMAN', 'is_agent': is_agent,
|
|
||||||
'reconnaissance_intent': is_agent, 'view_index': vi, 'total_views': len(viewed),
|
|
||||||
'theta': theta, 'trajectory_events': fevts, 'mdp_trajectory': events,
|
|
||||||
'demand_proxy': q, 'alpha_hat': alpha_hat, 'delta_h': delta_h, 'delta_a': delta_a}))
|
|
||||||
return opps
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AdversarialArrivalConfig:
|
|
||||||
base_rate: float = 5.0
|
|
||||||
n_parallel_agents: int = 3
|
|
||||||
query_all_products: bool = True
|
|
||||||
|
|
||||||
|
|
||||||
class AdversarialArrivalModel:
|
|
||||||
"""Adversarial coordination (Theorem 1): as N->inf, COI->0."""
|
|
||||||
|
|
||||||
def __init__(self, cfg: AdversarialArrivalConfig | None = None):
|
|
||||||
self.cfg = cfg or AdversarialArrivalConfig()
|
|
||||||
self._qcount = 0
|
|
||||||
|
|
||||||
def sample(self, t: float, dt: float, instruments: InstrumentSet,
|
|
||||||
market: MarketState | None, hidden: HiddenState, rng: np.random.Generator) -> list[Opportunity]:
|
|
||||||
cfg, opps = self.cfg, []
|
|
||||||
for _ in range(poisson_arrivals(cfg.base_rate, dt, rng)):
|
|
||||||
self._qcount += 1
|
|
||||||
for ai in range(cfg.n_parallel_agents):
|
|
||||||
sid = f"adv{self._qcount:06d}-{ai}"
|
|
||||||
prods = np.arange(instruments.n) if cfg.query_all_products else rng.choice(instruments.n, size=1)
|
|
||||||
for iid in prods:
|
|
||||||
opps.append(Opportunity(
|
|
||||||
id=f"{sid}-{iid}", type=OpportunityType.SESSION, side=Side.BUY,
|
|
||||||
instrument_id=int(iid), size=1.0, t=t,
|
|
||||||
context={'session_id': sid, 'actor_class': 'AGENT', 'is_agent': True, 'adversarial': True,
|
|
||||||
'agent_index': ai, 'query_group': self._qcount,
|
|
||||||
'theta': {'price_sensitivity': 0.0, 'base_conversion': 0.0, 'info_value': 1.0}}))
|
|
||||||
return opps
|
|
||||||
@@ -1,378 +0,0 @@
|
|||||||
"""Cost of Information (COI) computation for thesis pricing simulation.
|
|
||||||
|
|
||||||
Implements the corrected COI formulation:
|
|
||||||
|
|
||||||
COI = E[p] - p
|
|
||||||
|
|
||||||
where:
|
|
||||||
- E[p] = expected price BEFORE information revelation (window start price)
|
|
||||||
- p = actual transaction price (price at which sales occur)
|
|
||||||
|
|
||||||
The fundamental insight is that COI should measure PRICE EROSION over time,
|
|
||||||
not instantaneous margin leakage. When agents explore across sessions:
|
|
||||||
1. They reveal demand signals that drive platform price adjustments
|
|
||||||
2. Coordinated agents can find the minimum price across their session pool
|
|
||||||
3. The price path from window start to transaction captures information leakage
|
|
||||||
|
|
||||||
Key components:
|
|
||||||
- COIWindow: Windowed price erosion measurement over K steps
|
|
||||||
- compute_coi_window: Per-episode COI from session-level transactions
|
|
||||||
- coi_erosion: Order statistic erosion (Theorem 1: N agents -> min price)
|
|
||||||
|
|
||||||
This fixes the fundamental error of treating COI as instantaneous margin × alpha.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Dict, List, TYPE_CHECKING
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .simplified import Session
|
|
||||||
|
|
||||||
EPS = 1e-10
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class COIWindow:
|
|
||||||
"""Windowed COI measurement capturing price erosion over time.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
policy: Platform's intended COI (prices at window start - cost)
|
|
||||||
agent: Realized COI for agents (prices at transaction - cost)
|
|
||||||
leak: COI leakage = policy - agent (price erosion due to exploration)
|
|
||||||
survival_ratio: Fraction of intended COI that survives (agent/policy)
|
|
||||||
policy_by_product: Per-product policy COI
|
|
||||||
agent_by_product: Per-product agent COI
|
|
||||||
demand_weights: Demand weights used for aggregation
|
|
||||||
"""
|
|
||||||
policy: float = 0.0 # E[p] - c at window start
|
|
||||||
agent: float = 0.0 # p_transaction - c
|
|
||||||
leak: float = 0.0 # policy - agent = price erosion
|
|
||||||
survival_ratio: float = 1.0 # agent / policy
|
|
||||||
policy_by_product: np.ndarray = field(default_factory=lambda: np.zeros(1))
|
|
||||||
agent_by_product: np.ndarray = field(default_factory=lambda: np.zeros(1))
|
|
||||||
demand_weights: np.ndarray = field(default_factory=lambda: np.zeros(1))
|
|
||||||
|
|
||||||
def to_dict(self) -> Dict[str, float]:
|
|
||||||
return {
|
|
||||||
'coi_policy': self.policy,
|
|
||||||
'coi_agent': self.agent,
|
|
||||||
'coi_leak': self.leak,
|
|
||||||
'coi_survival': self.survival_ratio,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def compute_coi_window(
|
|
||||||
sessions: List["Session"],
|
|
||||||
costs: np.ndarray,
|
|
||||||
demand_mapping: Dict[str, float] = None,
|
|
||||||
window_prices: np.ndarray = None,
|
|
||||||
) -> COIWindow:
|
|
||||||
"""Compute COI from session data using the corrected formulation.
|
|
||||||
|
|
||||||
COI = E[p_start] - p_transaction
|
|
||||||
|
|
||||||
This measures how much the platform's pricing power eroded during the window.
|
|
||||||
Price at window start represents E[p] (what we expected to charge).
|
|
||||||
Transaction prices represent p (what we actually charged).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sessions: List of sessions with events containing price_seen and purchases
|
|
||||||
costs: Product costs array
|
|
||||||
demand_mapping: Optional session_id -> demand proxy mapping
|
|
||||||
window_prices: Optional explicit window start prices (otherwise use first seen)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
COIWindow with erosion metrics
|
|
||||||
"""
|
|
||||||
if not sessions:
|
|
||||||
n = len(costs)
|
|
||||||
zeros = np.zeros(n)
|
|
||||||
return COIWindow(policy=0.0, agent=0.0, leak=0.0, survival_ratio=1.0,
|
|
||||||
policy_by_product=zeros, agent_by_product=zeros, demand_weights=zeros)
|
|
||||||
|
|
||||||
n = len(costs)
|
|
||||||
demand_mapping = demand_mapping or {}
|
|
||||||
|
|
||||||
# Track prices seen at start (E[p]) and transaction prices (p)
|
|
||||||
first_prices = np.zeros(n) # first price seen per product (window start proxy)
|
|
||||||
transaction_prices = np.zeros(n) # prices at which purchases occurred
|
|
||||||
transaction_counts = np.zeros(n)
|
|
||||||
view_counts = np.zeros(n)
|
|
||||||
demand_weights = np.zeros(n)
|
|
||||||
|
|
||||||
for sess in sessions:
|
|
||||||
sid = sess.sid
|
|
||||||
sess_demand = demand_mapping.get(sid, 1.0)
|
|
||||||
|
|
||||||
for e in sess.events:
|
|
||||||
pidx = e.product_idx
|
|
||||||
if pidx < 0 or pidx >= n:
|
|
||||||
continue
|
|
||||||
|
|
||||||
price_seen = float(e.price_seen)
|
|
||||||
|
|
||||||
# Track first price seen (proxy for E[p] at window start)
|
|
||||||
if view_counts[pidx] == 0:
|
|
||||||
first_prices[pidx] = price_seen
|
|
||||||
view_counts[pidx] += 1
|
|
||||||
|
|
||||||
# Track transaction prices
|
|
||||||
if e.action == "purchase":
|
|
||||||
transaction_prices[pidx] += price_seen
|
|
||||||
transaction_counts[pidx] += 1
|
|
||||||
demand_weights[pidx] += sess_demand
|
|
||||||
|
|
||||||
# Compute per-product COI
|
|
||||||
# Policy COI: what we intended to charge (first seen price - cost)
|
|
||||||
policy_by_product = np.zeros(n)
|
|
||||||
agent_by_product = np.zeros(n)
|
|
||||||
|
|
||||||
for i in range(n):
|
|
||||||
if view_counts[i] > 0:
|
|
||||||
# Use explicit window prices if provided, else first seen
|
|
||||||
start_price = window_prices[i] if window_prices is not None else first_prices[i]
|
|
||||||
policy_by_product[i] = max(0, start_price - costs[i])
|
|
||||||
|
|
||||||
if transaction_counts[i] > 0:
|
|
||||||
avg_transaction = transaction_prices[i] / transaction_counts[i]
|
|
||||||
agent_by_product[i] = max(0, avg_transaction - costs[i])
|
|
||||||
|
|
||||||
# Aggregate with demand weighting
|
|
||||||
total_demand = np.sum(demand_weights) + EPS
|
|
||||||
weights = demand_weights / total_demand
|
|
||||||
|
|
||||||
# Only count products with transactions for fair comparison
|
|
||||||
active_mask = transaction_counts > 0
|
|
||||||
if np.any(active_mask):
|
|
||||||
policy = float(np.sum(policy_by_product[active_mask] * weights[active_mask]) /
|
|
||||||
(np.sum(weights[active_mask]) + EPS))
|
|
||||||
agent = float(np.sum(agent_by_product[active_mask] * weights[active_mask]) /
|
|
||||||
(np.sum(weights[active_mask]) + EPS))
|
|
||||||
else:
|
|
||||||
# No transactions - use view-weighted policy COI
|
|
||||||
view_weights = view_counts / (np.sum(view_counts) + EPS)
|
|
||||||
policy = float(np.sum(policy_by_product * view_weights))
|
|
||||||
agent = policy # No erosion without transactions
|
|
||||||
|
|
||||||
# Leak = price erosion due to information revelation
|
|
||||||
leak = max(0, policy - agent)
|
|
||||||
survival = agent / (policy + EPS) if policy > EPS else 1.0
|
|
||||||
|
|
||||||
return COIWindow(
|
|
||||||
policy=policy,
|
|
||||||
agent=agent,
|
|
||||||
leak=leak,
|
|
||||||
survival_ratio=float(np.clip(survival, 0, 1)),
|
|
||||||
policy_by_product=policy_by_product,
|
|
||||||
agent_by_product=agent_by_product,
|
|
||||||
demand_weights=demand_weights,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def coi_erosion(policy_coi: float, agent_coi: float) -> float:
|
|
||||||
"""Compute COI erosion rate: (policy - agent) / policy.
|
|
||||||
|
|
||||||
Returns the fraction of intended COI that was lost to information leakage.
|
|
||||||
0 = no erosion, 1 = complete erosion.
|
|
||||||
"""
|
|
||||||
if policy_coi < EPS:
|
|
||||||
return 0.0
|
|
||||||
return float(np.clip((policy_coi - agent_coi) / policy_coi, 0, 1))
|
|
||||||
|
|
||||||
|
|
||||||
def order_statistic_erosion(n_agents: int, price_std: float, base_margin: float = 1.0) -> float:
|
|
||||||
"""Compute COI erosion from order statistic effect (Theorem 1).
|
|
||||||
|
|
||||||
When N agents independently query prices:
|
|
||||||
- Each sees a price p_i ~ N(μ, σ²)
|
|
||||||
- They coordinate to buy at min(p_1, ..., p_N)
|
|
||||||
- Expected minimum: μ - σ * E[order_stat]
|
|
||||||
|
|
||||||
As N -> ∞, E[min] -> p_min, so COI -> 0.
|
|
||||||
|
|
||||||
This quantifies the price discovery benefit of multiple sessions.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
n_agents: Number of independent agent sessions
|
|
||||||
price_std: Standard deviation of price distribution
|
|
||||||
base_margin: Expected margin (μ - cost)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Erosion rate in [0, 1]
|
|
||||||
"""
|
|
||||||
if n_agents <= 1 or price_std < EPS:
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
# For standard normal order statistics, E[min of N] ≈ -Φ^{-1}(1/(N+1))
|
|
||||||
# For large N, this grows like sqrt(2 * log(N))
|
|
||||||
log_n = np.log(n_agents)
|
|
||||||
if log_n < 0.1:
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
# Extreme value theory: expected min shift
|
|
||||||
shift = price_std * (np.sqrt(2 * log_n) -
|
|
||||||
(np.log(log_n) + np.log(4 * np.pi)) / (2 * np.sqrt(2 * log_n) + EPS))
|
|
||||||
|
|
||||||
# Erosion = shift / base_margin, capped at 1
|
|
||||||
return float(np.clip(shift / (base_margin + EPS), 0, 1))
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class COITracker:
|
|
||||||
"""Track COI over multiple windows for temporal analysis.
|
|
||||||
|
|
||||||
This addresses the user's insight: compute COI over K episodes to see
|
|
||||||
how prices change from window start to end.
|
|
||||||
|
|
||||||
If at start of window price is A and by end it's B, the difference
|
|
||||||
A - B represents COI leakage from exploratory sessions.
|
|
||||||
"""
|
|
||||||
window_size: int = 10 # K episodes per window
|
|
||||||
_price_history: List[np.ndarray] = field(default_factory=list)
|
|
||||||
_transaction_history: List[np.ndarray] = field(default_factory=list)
|
|
||||||
_coi_history: List[float] = field(default_factory=list)
|
|
||||||
|
|
||||||
def add_step(self, prices: np.ndarray, transactions: np.ndarray = None):
|
|
||||||
"""Record price observation for current step."""
|
|
||||||
self._price_history.append(prices.copy())
|
|
||||||
if transactions is not None:
|
|
||||||
self._transaction_history.append(transactions.copy())
|
|
||||||
|
|
||||||
def compute_window_coi(self, costs: np.ndarray) -> float:
|
|
||||||
"""Compute COI over the current window.
|
|
||||||
|
|
||||||
COI = E[p_start] - E[p_end] for the window.
|
|
||||||
This captures price erosion due to information revelation.
|
|
||||||
"""
|
|
||||||
if len(self._price_history) < 2:
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
# Get prices at window boundaries
|
|
||||||
window_start = max(0, len(self._price_history) - self.window_size)
|
|
||||||
start_prices = self._price_history[window_start]
|
|
||||||
end_prices = self._price_history[-1]
|
|
||||||
|
|
||||||
# COI = (start_price - cost) - (end_price - cost) = start_price - end_price
|
|
||||||
start_margin = np.mean(start_prices - costs)
|
|
||||||
end_margin = np.mean(end_prices - costs)
|
|
||||||
|
|
||||||
coi = max(0, start_margin - end_margin)
|
|
||||||
self._coi_history.append(coi)
|
|
||||||
return coi
|
|
||||||
|
|
||||||
def get_cumulative_erosion(self, costs: np.ndarray) -> float:
|
|
||||||
"""Compute total COI erosion from first observation to now."""
|
|
||||||
if len(self._price_history) < 2:
|
|
||||||
return 0.0
|
|
||||||
|
|
||||||
initial = np.mean(self._price_history[0] - costs)
|
|
||||||
current = np.mean(self._price_history[-1] - costs)
|
|
||||||
return max(0, initial - current)
|
|
||||||
|
|
||||||
def get_erosion_trend(self) -> float:
|
|
||||||
"""Get average COI per window (erosion rate)."""
|
|
||||||
if not self._coi_history:
|
|
||||||
return 0.0
|
|
||||||
return float(np.mean(self._coi_history))
|
|
||||||
|
|
||||||
def reset(self):
|
|
||||||
"""Reset tracker for new episode."""
|
|
||||||
self._price_history.clear()
|
|
||||||
self._transaction_history.clear()
|
|
||||||
self._coi_history.clear()
|
|
||||||
|
|
||||||
|
|
||||||
def compute_multi_session_coi(
|
|
||||||
sessions: List["Session"],
|
|
||||||
costs: np.ndarray,
|
|
||||||
alpha: float,
|
|
||||||
initial_prices: np.ndarray,
|
|
||||||
) -> Dict[str, float]:
|
|
||||||
"""Compute COI accounting for multi-session agent behavior.
|
|
||||||
|
|
||||||
This is the key fix for the fundamental error:
|
|
||||||
- Agents use different sessions to gather information
|
|
||||||
- Each session reveals price information
|
|
||||||
- Coordinated agents find the minimum across their session pool
|
|
||||||
|
|
||||||
The COI is computed as:
|
|
||||||
1. What platform intended to charge: initial_prices - costs
|
|
||||||
2. What agents actually paid: min(prices seen across sessions) - costs
|
|
||||||
3. Leak = (1) - (2)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sessions: All sessions in the episode
|
|
||||||
costs: Product costs
|
|
||||||
alpha: Contamination level (fraction of agent sessions)
|
|
||||||
initial_prices: Prices at episode start (E[p])
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary with COI metrics
|
|
||||||
"""
|
|
||||||
n = len(costs)
|
|
||||||
|
|
||||||
# Separate agent and human sessions by ground truth label
|
|
||||||
agent_sessions = [s for s in sessions if s.actor == "A"]
|
|
||||||
human_sessions = [s for s in sessions if s.actor == "H"]
|
|
||||||
|
|
||||||
# Track prices seen by agents per product (for min finding)
|
|
||||||
agent_prices_seen: Dict[int, List[float]] = {i: [] for i in range(n)}
|
|
||||||
human_prices_paid: Dict[int, List[float]] = {i: [] for i in range(n)}
|
|
||||||
|
|
||||||
for sess in agent_sessions:
|
|
||||||
for e in sess.events:
|
|
||||||
if 0 <= e.product_idx < n:
|
|
||||||
agent_prices_seen[e.product_idx].append(e.price_seen)
|
|
||||||
|
|
||||||
for sess in human_sessions:
|
|
||||||
for e in sess.events:
|
|
||||||
if 0 <= e.product_idx < n and e.action == "purchase":
|
|
||||||
human_prices_paid[e.product_idx].append(e.price_seen)
|
|
||||||
|
|
||||||
# Compute COI components
|
|
||||||
policy_coi = float(np.mean(initial_prices - costs)) # E[p] - c
|
|
||||||
|
|
||||||
# Agent COI: they find the minimum price via exploration
|
|
||||||
agent_coi_by_product = np.zeros(n)
|
|
||||||
for i in range(n):
|
|
||||||
if agent_prices_seen[i]:
|
|
||||||
min_price = min(agent_prices_seen[i])
|
|
||||||
agent_coi_by_product[i] = max(0, min_price - costs[i])
|
|
||||||
else:
|
|
||||||
agent_coi_by_product[i] = initial_prices[i] - costs[i]
|
|
||||||
|
|
||||||
agent_coi = float(np.mean(agent_coi_by_product))
|
|
||||||
|
|
||||||
# Human COI: they pay whatever price is offered
|
|
||||||
human_coi_by_product = np.zeros(n)
|
|
||||||
for i in range(n):
|
|
||||||
if human_prices_paid[i]:
|
|
||||||
avg_price = np.mean(human_prices_paid[i])
|
|
||||||
human_coi_by_product[i] = max(0, avg_price - costs[i])
|
|
||||||
else:
|
|
||||||
human_coi_by_product[i] = initial_prices[i] - costs[i]
|
|
||||||
|
|
||||||
human_coi = float(np.mean(human_coi_by_product))
|
|
||||||
|
|
||||||
# Total leak: weighted by contamination
|
|
||||||
# Agents erode COI, humans pay full price
|
|
||||||
realized_coi = (1 - alpha) * human_coi + alpha * agent_coi
|
|
||||||
leak = policy_coi - realized_coi
|
|
||||||
|
|
||||||
# Order statistic effect: more agents = more erosion
|
|
||||||
n_agents = len(agent_sessions)
|
|
||||||
price_std = float(np.std(initial_prices))
|
|
||||||
order_erosion = order_statistic_erosion(n_agents, price_std, policy_coi)
|
|
||||||
|
|
||||||
return {
|
|
||||||
'policy_coi': policy_coi,
|
|
||||||
'agent_coi': agent_coi,
|
|
||||||
'human_coi': human_coi,
|
|
||||||
'realized_coi': realized_coi,
|
|
||||||
'leak': leak,
|
|
||||||
'order_stat_erosion': order_erosion,
|
|
||||||
'n_agent_sessions': n_agents,
|
|
||||||
'n_human_sessions': len(human_sessions),
|
|
||||||
'survival_ratio': realized_coi / (policy_coi + EPS) if policy_coi > EPS else 1.0,
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
"""Execution models with divergent H/A behavior using ground truth labels."""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any, Dict
|
|
||||||
import numpy as np
|
|
||||||
from ...outlet.types import Opportunity, Quote, InstrumentSet, MarketState
|
|
||||||
from ...outlet.math_util import sigmoid, safe_log, EPS
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class HybridExecutionConfig:
|
|
||||||
human_base_prob: float = 0.3
|
|
||||||
human_elasticity: float = 2.5
|
|
||||||
agent_conversion: float = 0.01
|
|
||||||
cross_elasticity: float = 0.4
|
|
||||||
quality_weight: float = 0.2
|
|
||||||
use_separability: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
class HybridExecutionModel:
|
|
||||||
"""Execution with divergent H/A behavior using ground truth labels."""
|
|
||||||
|
|
||||||
def __init__(self, cfg: HybridExecutionConfig | None = None):
|
|
||||||
self.cfg = cfg or HybridExecutionConfig()
|
|
||||||
|
|
||||||
def prob(self, opp: Opportunity, quote: Quote, instruments: InstrumentSet,
|
|
||||||
market: MarketState | None, rng: np.random.Generator) -> float:
|
|
||||||
cfg, idx = self.cfg, int(opp.instrument_id)
|
|
||||||
price, ref, cost = float(quote.prices[idx]), float(instruments.refs[idx]), float(instruments.costs[idx])
|
|
||||||
ctx = opp.context
|
|
||||||
theta = ctx.get('theta', {})
|
|
||||||
is_agent = ctx.get('is_agent', False)
|
|
||||||
|
|
||||||
if is_agent:
|
|
||||||
return cfg.agent_conversion * theta.get('base_conversion', 1.0)
|
|
||||||
|
|
||||||
# human logit discrete choice
|
|
||||||
sens = theta.get('price_sensitivity', cfg.human_elasticity)
|
|
||||||
base = theta.get('base_conversion', cfg.human_base_prob)
|
|
||||||
u_price = -sens * safe_log(price / (ref + EPS))
|
|
||||||
quality = instruments.instruments[idx].attrs.get('quality', 0.5)
|
|
||||||
u_quality = cfg.quality_weight * quality
|
|
||||||
|
|
||||||
u_comp = 0.0
|
|
||||||
if market and market.competitor_quotes is not None:
|
|
||||||
cp = market.competitor_quotes[idx]
|
|
||||||
if cp < price:
|
|
||||||
u_comp = -cfg.cross_elasticity * (price - cp) / ref
|
|
||||||
|
|
||||||
utility = safe_log(base / (1 - base + EPS)) + u_price + u_quality + u_comp
|
|
||||||
return float(sigmoid(utility))
|
|
||||||
|
|
||||||
def uncensor(self, fills: np.ndarray, instruments: InstrumentSet, context: dict[str, Any] | None = None) -> np.ndarray:
|
|
||||||
if context is None:
|
|
||||||
return fills / (self.cfg.human_base_prob + EPS)
|
|
||||||
agent_frac = context.get('contamination', 0.0)
|
|
||||||
return fills / (self.cfg.human_base_prob * (1 - agent_frac) + EPS)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SeparableExecutionConfig:
|
|
||||||
human_funnel: Dict[str, float] = None
|
|
||||||
agent_funnel: Dict[str, float] = None
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
self.human_funnel = self.human_funnel or {'view_to_detail': 0.4, 'detail_to_cart': 0.3, 'cart_to_purchase': 0.6}
|
|
||||||
self.agent_funnel = self.agent_funnel or {'view_to_detail': 0.8, 'detail_to_cart': 0.05, 'cart_to_purchase': 0.1}
|
|
||||||
|
|
||||||
|
|
||||||
class SeparableExecutionModel:
|
|
||||||
"""Execution with Markov funnel kernels using ground truth labels."""
|
|
||||||
|
|
||||||
def __init__(self, cfg: SeparableExecutionConfig | None = None):
|
|
||||||
self.cfg = cfg or SeparableExecutionConfig()
|
|
||||||
|
|
||||||
def prob(self, opp: Opportunity, quote: Quote, instruments: InstrumentSet,
|
|
||||||
market: MarketState | None, rng: np.random.Generator) -> float:
|
|
||||||
is_agent = opp.context.get('is_agent', False)
|
|
||||||
probs = self.cfg.agent_funnel if is_agent else self.cfg.human_funnel
|
|
||||||
p = probs['view_to_detail'] * probs['detail_to_cart'] * probs['cart_to_purchase']
|
|
||||||
|
|
||||||
if not is_agent:
|
|
||||||
idx = int(opp.instrument_id)
|
|
||||||
price_ratio = quote.prices[idx] / (instruments.refs[idx] + EPS)
|
|
||||||
p *= np.exp(-0.5 * (price_ratio - 1.0))
|
|
||||||
return float(np.clip(p, 0, 1))
|
|
||||||
|
|
||||||
def uncensor(self, fills: np.ndarray, instruments: InstrumentSet, context: dict[str, Any] | None = None) -> np.ndarray:
|
|
||||||
h = self.cfg.human_funnel
|
|
||||||
exp_conv = h['view_to_detail'] * h['detail_to_cart'] * h['cart_to_purchase']
|
|
||||||
return fills / (exp_conv + EPS)
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
"""Thesis metrics for COI and behavioral analysis using ground truth labels."""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Dict
|
|
||||||
import numpy as np
|
|
||||||
from ...outlet.types import StepLogs, StepMetrics, Quote, InstrumentSet
|
|
||||||
from ...outlet.math_util import safe_log, EPS
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class COIMetrics:
|
|
||||||
coi_level: float = 0.0
|
|
||||||
coi_leakage: float = 0.0
|
|
||||||
realized_premium: float = 0.0
|
|
||||||
theoretical_max: float = 0.0
|
|
||||||
erosion_rate: float = 0.0
|
|
||||||
|
|
||||||
def to_dict(self) -> dict[str, float]:
|
|
||||||
return {k: getattr(self, k) for k in ['coi_level', 'coi_leakage', 'realized_premium', 'theoretical_max', 'erosion_rate']}
|
|
||||||
|
|
||||||
|
|
||||||
def compute_coi(quote: Quote, instruments: InstrumentSet, metrics: StepMetrics, contamination: float) -> COIMetrics:
|
|
||||||
prices, costs, refs = quote.prices, instruments.costs, instruments.refs
|
|
||||||
margins = prices - costs
|
|
||||||
coi_level = float(np.mean(margins))
|
|
||||||
theoretical_max = float(np.mean(costs))
|
|
||||||
realized_premium = (metrics.revenue - metrics.cost) / metrics.units_traded if metrics.units_traded > 0 else 0.0
|
|
||||||
price_var = float(np.var(prices / refs))
|
|
||||||
coi_leakage = contamination * (coi_level + price_var)
|
|
||||||
erosion_rate = contamination * coi_level / (theoretical_max + EPS)
|
|
||||||
return COIMetrics(coi_level=coi_level, coi_leakage=coi_leakage, realized_premium=realized_premium,
|
|
||||||
theoretical_max=theoretical_max, erosion_rate=erosion_rate)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SeparabilityMetrics:
|
|
||||||
classification_accuracy: float = 0.0
|
|
||||||
estimated_alpha: float = 0.0
|
|
||||||
n_human_sessions: int = 0
|
|
||||||
n_agent_sessions: int = 0
|
|
||||||
|
|
||||||
|
|
||||||
def compute_separability(logs: StepLogs, true_alpha: float) -> SeparabilityMetrics:
|
|
||||||
"""Compute separability using ground truth labels only."""
|
|
||||||
if logs.events is None or len(logs.events) == 0:
|
|
||||||
return SeparabilityMetrics(estimated_alpha=true_alpha)
|
|
||||||
|
|
||||||
sessions: Dict[str, bool] = {}
|
|
||||||
for evt in logs.events:
|
|
||||||
sid = evt.metadata.get('session_id', evt.opportunity_id)
|
|
||||||
if sid not in sessions:
|
|
||||||
sessions[sid] = evt.metadata.get('is_agent', False)
|
|
||||||
|
|
||||||
n_agent = sum(1 for is_agent in sessions.values() if is_agent)
|
|
||||||
n_human = len(sessions) - n_agent
|
|
||||||
est_alpha = n_agent / len(sessions) if sessions else 0.0
|
|
||||||
|
|
||||||
return SeparabilityMetrics(
|
|
||||||
classification_accuracy=1.0, # ground truth is always correct
|
|
||||||
estimated_alpha=est_alpha,
|
|
||||||
n_human_sessions=n_human,
|
|
||||||
n_agent_sessions=n_agent)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RevenueAttribution:
|
|
||||||
total_revenue: float = 0.0
|
|
||||||
human_revenue: float = 0.0
|
|
||||||
agent_revenue: float = 0.0
|
|
||||||
human_conversion: float = 0.0
|
|
||||||
agent_conversion: float = 0.0
|
|
||||||
|
|
||||||
|
|
||||||
def compute_attribution(logs: StepLogs, metrics: StepMetrics) -> RevenueAttribution:
|
|
||||||
if logs.executions is None:
|
|
||||||
return RevenueAttribution(total_revenue=metrics.revenue)
|
|
||||||
|
|
||||||
human_rev, agent_rev, human_cnt, agent_cnt = 0.0, 0.0, 0, 0
|
|
||||||
for exe in logs.executions:
|
|
||||||
if exe.propensity < 0.05:
|
|
||||||
agent_rev += exe.price * exe.size_filled
|
|
||||||
agent_cnt += 1
|
|
||||||
else:
|
|
||||||
human_rev += exe.price * exe.size_filled
|
|
||||||
human_cnt += 1
|
|
||||||
|
|
||||||
total_exp = logs.aggregates.get('n_arrivals', 1)
|
|
||||||
return RevenueAttribution(
|
|
||||||
total_revenue=metrics.revenue, human_revenue=human_rev, agent_revenue=agent_rev,
|
|
||||||
human_conversion=human_cnt / (total_exp * 0.8 + EPS),
|
|
||||||
agent_conversion=agent_cnt / (total_exp * 0.2 + EPS))
|
|
||||||
|
|
||||||
|
|
||||||
def order_statistic_erosion(n_agents: int, price_variance: float) -> float:
|
|
||||||
"""COI erosion from Theorem 1: as N->inf, min(p_1..p_N)->p_min."""
|
|
||||||
if n_agents <= 1:
|
|
||||||
return 0.0
|
|
||||||
sigma, log_n = np.sqrt(price_variance), safe_log(n_agents)
|
|
||||||
if log_n < 1:
|
|
||||||
return 0.0
|
|
||||||
shift = sigma * (np.sqrt(2 * log_n) - (safe_log(log_n) + safe_log(4 * np.pi)) / (2 * np.sqrt(2 * log_n) + EPS))
|
|
||||||
return float(min(shift / (sigma * 2 + EPS), 1.0))
|
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
"""
|
|
||||||
Thesis-specific objectives implementing robust pricing under contamination.
|
|
||||||
|
|
||||||
Implements the Maximin objective from Eq 23:
|
|
||||||
π* = argmax_π min_{Q ∈ U_ε} E_d~Q[R(p,d) - λ·COI(p)]
|
|
||||||
|
|
||||||
Key components:
|
|
||||||
- COIObjective: Cost of Information penalty (Definition 1)
|
|
||||||
- RobustStackelbergObjective: Full maximin objective with Wasserstein robustness
|
|
||||||
- UXPenalty: User experience degradation from volatility
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import numpy as np
|
|
||||||
from ...outlet.objectives.base import BaseObjective, CompositeObjective
|
|
||||||
from ...outlet.types import Quote, InstrumentSet, StepMetrics, HiddenState, Observation
|
|
||||||
from ...outlet.math_util import safe_log, EPS
|
|
||||||
|
|
||||||
class COIObjective(BaseObjective):
|
|
||||||
"""Cost of Information penalty from Definition 1.
|
|
||||||
|
|
||||||
COI(π) = E[P] - p_min
|
|
||||||
|
|
||||||
The expected price premium over marginal cost represents the platform's
|
|
||||||
pricing power. Agent reconnaissance erodes this by revealing price
|
|
||||||
distribution to buyers.
|
|
||||||
|
|
||||||
We implement COI_leakage = f(τ') · InfoValue(p, τ')
|
|
||||||
where f(τ') is the estimated agent probability.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, lambda_coi: float = 1.0, use_revelation: bool = False):
|
|
||||||
"""
|
|
||||||
Args:
|
|
||||||
lambda_coi: Weight on COI penalty
|
|
||||||
use_revelation: If True, use -log(π(p)) as info value (penalizes rare prices)
|
|
||||||
"""
|
|
||||||
self.lambda_coi = lambda_coi
|
|
||||||
self.use_revelation = use_revelation
|
|
||||||
|
|
||||||
def reward(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> float:
|
|
||||||
# COI_leakage = α · InfoValue
|
|
||||||
alpha = hidden.contamination
|
|
||||||
|
|
||||||
if self.use_revelation:
|
|
||||||
# revelation surrogate: rare prices reveal more about policy
|
|
||||||
# InfoValue = -log(π(p|τ')) ≈ surprise of the price
|
|
||||||
price_surprise = np.mean(np.abs(quote.prices - instruments.refs) / (instruments.refs + EPS))
|
|
||||||
info_value = price_surprise
|
|
||||||
else:
|
|
||||||
# query-tax surrogate: each agent query incurs constant leakage
|
|
||||||
info_value = 1.0
|
|
||||||
|
|
||||||
leakage = alpha * info_value
|
|
||||||
return -self.lambda_coi * leakage
|
|
||||||
|
|
||||||
def breakdown(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> dict[str, float]:
|
|
||||||
alpha = hidden.contamination
|
|
||||||
margins = (quote.prices - instruments.costs) / (instruments.costs + EPS)
|
|
||||||
return {
|
|
||||||
'coi_penalty': self.reward(quote, instruments, metrics, hidden, obs),
|
|
||||||
'contamination': alpha,
|
|
||||||
'avg_margin': float(np.mean(margins)),
|
|
||||||
}
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RobustObjectiveConfig:
|
|
||||||
"""Configuration for robust Stackelberg objective.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
lambda_coi: Weight on COI penalty (λ in Eq 23)
|
|
||||||
lambda_ux: Weight on UX penalty
|
|
||||||
lambda_volatility: Weight on price volatility penalty
|
|
||||||
gamma_inventory: Inventory risk aversion
|
|
||||||
wasserstein_epsilon: Ambiguity set radius (ε in Eq 21)
|
|
||||||
"""
|
|
||||||
lambda_coi: float = 0.5
|
|
||||||
lambda_ux: float = 0.1
|
|
||||||
lambda_volatility: float = 0.2
|
|
||||||
gamma_inventory: float = 0.1
|
|
||||||
wasserstein_epsilon: float = 0.1
|
|
||||||
|
|
||||||
class RobustStackelbergObjective(BaseObjective):
|
|
||||||
"""Implements the Maximin Objective from thesis Eq 23.
|
|
||||||
|
|
||||||
π* = argmax_π min_{Q ∈ U_ε(P̂_N)} E_d~Q[R(p,d) - λ·COI(p)]
|
|
||||||
|
|
||||||
The objective balances:
|
|
||||||
1. Revenue R(p,d) from human purchases
|
|
||||||
2. COI penalty for information leakage to agents
|
|
||||||
3. UX penalty for price volatility
|
|
||||||
4. Inventory/holding costs
|
|
||||||
|
|
||||||
The min over ambiguity set U_ε is approximated by penalizing
|
|
||||||
high contamination scenarios more heavily.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cfg: RobustObjectiveConfig | None = None):
|
|
||||||
self.cfg = cfg or RobustObjectiveConfig()
|
|
||||||
|
|
||||||
def reward(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> float:
|
|
||||||
cfg = self.cfg
|
|
||||||
|
|
||||||
# 1. base revenue (R(p,d))
|
|
||||||
revenue = metrics.revenue
|
|
||||||
cost = metrics.cost
|
|
||||||
profit = revenue - cost
|
|
||||||
|
|
||||||
# 2. COI penalty: scales with contamination and margin extraction
|
|
||||||
# high margins + high contamination = high leakage
|
|
||||||
alpha = hidden.contamination
|
|
||||||
margins = quote.prices - instruments.costs
|
|
||||||
avg_margin = float(np.mean(margins))
|
|
||||||
coi_penalty = cfg.lambda_coi * avg_margin * alpha
|
|
||||||
|
|
||||||
# 3. UX penalty: price volatility harms legitimate users
|
|
||||||
volatility_penalty = cfg.lambda_volatility * metrics.volatility
|
|
||||||
|
|
||||||
# 4. inventory/position cost
|
|
||||||
position_penalty = cfg.gamma_inventory * metrics.position_cost
|
|
||||||
|
|
||||||
# 5. lost opportunity cost (stockouts)
|
|
||||||
lost_penalty = 0.1 * metrics.lost_opportunity
|
|
||||||
|
|
||||||
# robust adjustment: under adversarial distribution Q,
|
|
||||||
# expect lower revenue and higher costs
|
|
||||||
# approximate via worst-case contamination within ε-ball
|
|
||||||
worst_case_alpha = min(alpha + cfg.wasserstein_epsilon, 1.0)
|
|
||||||
robustness_penalty = cfg.wasserstein_epsilon * avg_margin * worst_case_alpha
|
|
||||||
|
|
||||||
total = profit - coi_penalty - volatility_penalty - position_penalty - lost_penalty - robustness_penalty
|
|
||||||
|
|
||||||
return total
|
|
||||||
|
|
||||||
def breakdown(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> dict[str, float]:
|
|
||||||
cfg = self.cfg
|
|
||||||
alpha = hidden.contamination
|
|
||||||
margins = quote.prices - instruments.costs
|
|
||||||
avg_margin = float(np.mean(margins))
|
|
||||||
|
|
||||||
return {
|
|
||||||
'revenue': metrics.revenue,
|
|
||||||
'cost': metrics.cost,
|
|
||||||
'profit': metrics.revenue - metrics.cost,
|
|
||||||
'coi_penalty': -cfg.lambda_coi * avg_margin * alpha,
|
|
||||||
'volatility_penalty': -cfg.lambda_volatility * metrics.volatility,
|
|
||||||
'position_penalty': -cfg.gamma_inventory * metrics.position_cost,
|
|
||||||
'lost_penalty': -0.1 * metrics.lost_opportunity,
|
|
||||||
'robustness_penalty': -cfg.wasserstein_epsilon * avg_margin * min(alpha + cfg.wasserstein_epsilon, 1.0),
|
|
||||||
'contamination': alpha,
|
|
||||||
'avg_margin_pct': avg_margin / (float(np.mean(instruments.costs)) + EPS),
|
|
||||||
}
|
|
||||||
|
|
||||||
class UXPenalty(BaseObjective):
|
|
||||||
"""User experience penalty from price volatility.
|
|
||||||
|
|
||||||
High price volatility degrades UX for legitimate human users.
|
|
||||||
This term ensures the defense doesn't harm real customers while
|
|
||||||
protecting against agent reconnaissance.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, scale: float = 1.0, max_acceptable_volatility: float = 0.1):
|
|
||||||
self.scale = scale
|
|
||||||
self.max_vol = max_acceptable_volatility
|
|
||||||
|
|
||||||
def reward(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> float:
|
|
||||||
# penalty increases quadratically beyond threshold
|
|
||||||
excess_vol = max(0, metrics.volatility - self.max_vol)
|
|
||||||
return -self.scale * (excess_vol ** 2)
|
|
||||||
|
|
||||||
def breakdown(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> dict[str, float]:
|
|
||||||
return {
|
|
||||||
'ux_penalty': self.reward(quote, instruments, metrics, hidden, obs),
|
|
||||||
'volatility': metrics.volatility,
|
|
||||||
}
|
|
||||||
|
|
||||||
class AdaptiveObjective(BaseObjective):
|
|
||||||
"""Objective that adapts weights based on estimated contamination.
|
|
||||||
|
|
||||||
When contamination is low, focus on revenue maximization.
|
|
||||||
When contamination is high, increase COI defense weight.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, base_lambda_coi: float = 0.3, max_lambda_coi: float = 2.0,
|
|
||||||
adaptation_rate: float = 2.0):
|
|
||||||
self.base_lambda = base_lambda_coi
|
|
||||||
self.max_lambda = max_lambda_coi
|
|
||||||
self.rate = adaptation_rate
|
|
||||||
|
|
||||||
def _adaptive_lambda(self, alpha: float) -> float:
|
|
||||||
# sigmoid scaling: λ(α) = base + (max-base) * sigmoid(rate*(α-0.5))
|
|
||||||
from ...outlet.math_util import sigmoid
|
|
||||||
scale = sigmoid(self.rate * (alpha - 0.3))
|
|
||||||
return self.base_lambda + (self.max_lambda - self.base_lambda) * scale
|
|
||||||
|
|
||||||
def reward(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> float:
|
|
||||||
alpha = hidden.contamination
|
|
||||||
lambda_coi = self._adaptive_lambda(alpha)
|
|
||||||
|
|
||||||
profit = metrics.revenue - metrics.cost
|
|
||||||
margins = quote.prices - instruments.costs
|
|
||||||
coi_penalty = lambda_coi * float(np.mean(margins)) * alpha
|
|
||||||
|
|
||||||
return profit - coi_penalty
|
|
||||||
|
|
||||||
def breakdown(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> dict[str, float]:
|
|
||||||
alpha = hidden.contamination
|
|
||||||
return {
|
|
||||||
'profit': metrics.revenue - metrics.cost,
|
|
||||||
'adaptive_lambda': self._adaptive_lambda(alpha),
|
|
||||||
'contamination': alpha,
|
|
||||||
}
|
|
||||||
|
|
||||||
def make_thesis_objective(lambda_coi: float = 0.5, lambda_ux: float = 0.1,
|
|
||||||
lambda_vol: float = 0.2) -> CompositeObjective:
|
|
||||||
"""Create the standard thesis objective composition."""
|
|
||||||
return CompositeObjective([
|
|
||||||
(RobustStackelbergObjective(RobustObjectiveConfig(
|
|
||||||
lambda_coi=lambda_coi, lambda_ux=lambda_ux, lambda_volatility=lambda_vol)), 1.0),
|
|
||||||
])
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
"""Thesis platform with real MDP behavioral models and separability scoring."""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
import numpy as np
|
|
||||||
from ...outlet import (Platform, PlatformConfig, PositionModel, PositionConfig,
|
|
||||||
PostedPriceMechanism, make_instruments, InstrumentType, LogLevel)
|
|
||||||
from ...outlet.mechanisms.posted_price import PostedPriceConfig
|
|
||||||
from ...outlet.observation import DefaultObservationBuilder, ObservationConfig
|
|
||||||
from .arrivals import ContaminatedArrivalModel, ContaminatedArrivalConfig
|
|
||||||
from .execution import HybridExecutionModel, HybridExecutionConfig
|
|
||||||
from .objectives import RobustStackelbergObjective, RobustObjectiveConfig
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ThesisConfig:
|
|
||||||
# instruments
|
|
||||||
n_instruments: int = 10
|
|
||||||
cost_range: tuple[float, float] = (5.0, 50.0)
|
|
||||||
margin_range: tuple[float, float] = (0.2, 0.5)
|
|
||||||
|
|
||||||
# contamination (Section 3.1)
|
|
||||||
alpha_contamination: float = 0.2
|
|
||||||
alpha_drift: float = 0.0
|
|
||||||
alpha_bounds: tuple[float, float] = (0.0, 0.5)
|
|
||||||
|
|
||||||
# objectives (Eq 23)
|
|
||||||
lambda_coi: float = 0.5
|
|
||||||
lambda_ux: float = 0.1
|
|
||||||
lambda_volatility: float = 0.2
|
|
||||||
wasserstein_epsilon: float = 0.1
|
|
||||||
|
|
||||||
# arrivals
|
|
||||||
sessions_per_step: int = 30
|
|
||||||
human_views_range: tuple[int, int] = (1, 4)
|
|
||||||
agent_views_range: tuple[int, int] = (3, 10)
|
|
||||||
|
|
||||||
# inventory
|
|
||||||
initial_inventory: float = 100.0
|
|
||||||
holding_cost_rate: float = 0.002
|
|
||||||
|
|
||||||
# real behavioral models (from sim.rl)
|
|
||||||
use_real_behavior: bool = True
|
|
||||||
use_separability: bool = False # disabled until classifier trained
|
|
||||||
human_data_dir: str = "/home/velocitatem/Documents/Projects/PHANTOM/experiments/collected_data"
|
|
||||||
agent_data_dir: str = "/home/velocitatem/Documents/Projects/PHANTOM/experiments/agents/collected_data"
|
|
||||||
|
|
||||||
# simulation
|
|
||||||
max_steps: int = 500
|
|
||||||
seed: int | None = 24
|
|
||||||
log_level: LogLevel = LogLevel.AGG_ONLY
|
|
||||||
|
|
||||||
|
|
||||||
def _resolve_data_dirs(cfg: ThesisConfig) -> tuple[str, str]:
|
|
||||||
"""Resolve data directories for behavioral models."""
|
|
||||||
base = Path(__file__).parent.parent.parent.parent / "experiments"
|
|
||||||
human = cfg.human_data_dir or str(base / "collected_data")
|
|
||||||
agent = cfg.agent_data_dir or str(base / "agents/collected_data")
|
|
||||||
return human, agent
|
|
||||||
|
|
||||||
|
|
||||||
def make_thesis_platform(cfg: ThesisConfig | None = None) -> Platform:
|
|
||||||
"""Create platform with real MDP behavioral models.
|
|
||||||
|
|
||||||
Implements:
|
|
||||||
- Contaminated arrivals using learned MDP kernels from behavior_loader
|
|
||||||
- Hybrid execution with real separability scoring from lib.separability
|
|
||||||
- Robust Stackelberg objective (Eq 23)
|
|
||||||
"""
|
|
||||||
cfg = cfg or ThesisConfig()
|
|
||||||
rng = np.random.default_rng(cfg.seed)
|
|
||||||
human_dir, agent_dir = _resolve_data_dirs(cfg)
|
|
||||||
|
|
||||||
instruments = make_instruments(
|
|
||||||
n=cfg.n_instruments, cost_range=cfg.cost_range, margin_range=cfg.margin_range,
|
|
||||||
inst_type=InstrumentType.SKU, rng=rng)
|
|
||||||
instruments.position = np.full(cfg.n_instruments, cfg.initial_inventory)
|
|
||||||
|
|
||||||
arrival = ContaminatedArrivalModel(ContaminatedArrivalConfig(
|
|
||||||
base_rate=cfg.sessions_per_step,
|
|
||||||
alpha_contamination=cfg.alpha_contamination,
|
|
||||||
alpha_drift=cfg.alpha_drift,
|
|
||||||
alpha_bounds=cfg.alpha_bounds,
|
|
||||||
human_views_range=cfg.human_views_range,
|
|
||||||
agent_views_range=cfg.agent_views_range,
|
|
||||||
use_real_behavior=cfg.use_real_behavior,
|
|
||||||
human_data_dir=human_dir,
|
|
||||||
agent_data_dir=agent_dir,
|
|
||||||
))
|
|
||||||
|
|
||||||
execution = HybridExecutionModel(HybridExecutionConfig(
|
|
||||||
use_separability=cfg.use_separability,
|
|
||||||
))
|
|
||||||
|
|
||||||
mechanism = PostedPriceMechanism(PostedPriceConfig(max_delta_pct=0.15, min_margin_pct=0.05))
|
|
||||||
position = PositionModel(PositionConfig(initial_position=cfg.initial_inventory, holding_cost_rate=cfg.holding_cost_rate))
|
|
||||||
|
|
||||||
market = None
|
|
||||||
objective = RobustStackelbergObjective(RobustObjectiveConfig(
|
|
||||||
lambda_coi=cfg.lambda_coi, lambda_ux=cfg.lambda_ux,
|
|
||||||
lambda_volatility=cfg.lambda_volatility, wasserstein_epsilon=cfg.wasserstein_epsilon))
|
|
||||||
|
|
||||||
obs_builder = DefaultObservationBuilder(ObservationConfig(mask_true_demand=True))
|
|
||||||
platform_cfg = PlatformConfig(n_instruments=cfg.n_instruments, max_steps=cfg.max_steps,
|
|
||||||
seed=cfg.seed, log_level=cfg.log_level, mask_demand=True)
|
|
||||||
|
|
||||||
return Platform(instruments=instruments, mechanism=mechanism, arrival=arrival, execution=execution,
|
|
||||||
position=position, market=market, obs_builder=obs_builder, objective=objective, cfg=platform_cfg)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AblationConfig(ThesisConfig):
|
|
||||||
disable_coi_penalty: bool = False
|
|
||||||
disable_ux_penalty: bool = False
|
|
||||||
disable_contamination: bool = False
|
|
||||||
disable_real_behavior: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
def make_ablation_platform(cfg: AblationConfig) -> Platform:
|
|
||||||
if cfg.disable_coi_penalty:
|
|
||||||
cfg.lambda_coi = 0.0
|
|
||||||
if cfg.disable_ux_penalty:
|
|
||||||
cfg.lambda_ux = 0.0
|
|
||||||
if cfg.disable_contamination:
|
|
||||||
cfg.alpha_contamination = 0.0
|
|
||||||
if cfg.disable_real_behavior:
|
|
||||||
cfg.use_real_behavior = False
|
|
||||||
cfg.use_separability = False
|
|
||||||
return make_thesis_platform(cfg)
|
|
||||||
|
|
||||||
|
|
||||||
def sweep_contamination(alpha_values: list[float], base_cfg: ThesisConfig | None = None,
|
|
||||||
n_steps: int = 100, seed: int = 42) -> dict[float, dict]:
|
|
||||||
"""Test performance across contamination levels (Theorem 1 validation)."""
|
|
||||||
from ...experiments.eval import rollout, fixed_price_policy
|
|
||||||
|
|
||||||
results = {}
|
|
||||||
base_cfg = base_cfg or ThesisConfig()
|
|
||||||
|
|
||||||
for alpha in alpha_values:
|
|
||||||
cfg = ThesisConfig(**{k: v for k, v in base_cfg.__dict__.items() if k != 'alpha_contamination'},
|
|
||||||
alpha_contamination=alpha)
|
|
||||||
platform = make_thesis_platform(cfg)
|
|
||||||
policy = fixed_price_policy(platform.instruments.refs)
|
|
||||||
result = rollout(platform, policy, n_steps, seed=seed)
|
|
||||||
results[alpha] = {
|
|
||||||
'total_reward': result.total_reward,
|
|
||||||
'total_pnl': result.total_pnl,
|
|
||||||
'avg_conversion': result.avg_conversion,
|
|
||||||
'final_contamination': platform._hidden.contamination,
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
def sweep_behavior_modes(base_cfg: ThesisConfig | None = None, n_steps: int = 100, seed: int = 42) -> dict[str, dict]:
|
|
||||||
"""Compare real vs synthetic behavioral models."""
|
|
||||||
from ...experiments.eval import rollout, fixed_price_policy
|
|
||||||
|
|
||||||
base_cfg = base_cfg or ThesisConfig()
|
|
||||||
modes = {
|
|
||||||
'real_mdp': ThesisConfig(**{**base_cfg.__dict__, 'use_real_behavior': True, 'use_separability': True}),
|
|
||||||
'synthetic': ThesisConfig(**{**base_cfg.__dict__, 'use_real_behavior': False, 'use_separability': False}),
|
|
||||||
'real_mdp_no_sep': ThesisConfig(**{**base_cfg.__dict__, 'use_real_behavior': True, 'use_separability': False}),
|
|
||||||
}
|
|
||||||
|
|
||||||
results = {}
|
|
||||||
for name, cfg in modes.items():
|
|
||||||
platform = make_thesis_platform(cfg)
|
|
||||||
policy = fixed_price_policy(platform.instruments.refs)
|
|
||||||
result = rollout(platform, policy, n_steps, seed=seed)
|
|
||||||
results[name] = {
|
|
||||||
'total_reward': result.total_reward,
|
|
||||||
'total_pnl': result.total_pnl,
|
|
||||||
'avg_conversion': result.avg_conversion,
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
"""Thesis simulation experiments with real MDP behavioral models."""
|
|
||||||
from __future__ import annotations
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent.parent.parent))
|
|
||||||
|
|
||||||
from lab.case.thesis.platform import make_thesis_platform, ThesisConfig
|
|
||||||
from lab.case.thesis.metrics import compute_coi, compute_separability
|
|
||||||
from lab.experiments.eval import compare_policies
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
|
|
||||||
def demo_basic_simulation():
|
|
||||||
print("=" * 70)
|
|
||||||
print("THESIS SIMULATION: Contaminated Dynamic Pricing (Real MDP Kernels)")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
cfg = ThesisConfig(n_instruments=5, alpha_contamination=0.3, lambda_coi=0.5,
|
|
||||||
max_steps=100, seed=42, use_real_behavior=True)
|
|
||||||
platform = make_thesis_platform(cfg)
|
|
||||||
|
|
||||||
print(f"\nInstruments: {platform.instruments.n}")
|
|
||||||
print(f"Reference prices: {platform.instruments.refs.round(2)}")
|
|
||||||
print(f"Costs: {platform.instruments.costs.round(2)}")
|
|
||||||
print(f"Initial contamination alpha={cfg.alpha_contamination}")
|
|
||||||
print(f"Using real behavior: {cfg.use_real_behavior}")
|
|
||||||
|
|
||||||
result = platform.reset(seed=42)
|
|
||||||
total_reward, coi_history = 0, []
|
|
||||||
|
|
||||||
print(f"\n{'Step':>5} {'Reward':>10} {'PnL':>10} {'COI':>8} {'alpha':>6} {'Conv':>8}")
|
|
||||||
print("-" * 55)
|
|
||||||
|
|
||||||
for t in range(cfg.max_steps):
|
|
||||||
action = platform.instruments.refs * np.random.uniform(0.95, 1.15, size=platform.instruments.n)
|
|
||||||
result = platform.step(action)
|
|
||||||
total_reward += result.reward
|
|
||||||
coi = compute_coi(platform._quote, platform.instruments, result.metrics, result.hidden.contamination)
|
|
||||||
coi_history.append(coi.coi_level)
|
|
||||||
|
|
||||||
if t % 20 == 0:
|
|
||||||
print(f"{t:5d} {result.reward:10.2f} {result.metrics.pnl:10.2f} "
|
|
||||||
f"{coi.coi_level:8.2f} {result.hidden.contamination:6.2f} {result.metrics.conversion:8.3f}")
|
|
||||||
|
|
||||||
print("-" * 55)
|
|
||||||
print(f"Total Reward: {total_reward:.2f}")
|
|
||||||
print(f"Average COI: {np.mean(coi_history):.2f}")
|
|
||||||
print(f"COI Trend: {coi_history[-1] - coi_history[0]:+.2f}")
|
|
||||||
|
|
||||||
|
|
||||||
def demo_contamination_sweep():
|
|
||||||
print("\n" + "=" * 70)
|
|
||||||
print("EXPERIMENT: COI Erosion vs Contamination (Theorem 1)")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
from lab.case.thesis.platform import sweep_contamination
|
|
||||||
trials = 20
|
|
||||||
alpha_values = [i/trials for i in range(trials)]
|
|
||||||
results = sweep_contamination(alpha_values, n_steps=100, seed=42)
|
|
||||||
|
|
||||||
print(f"\n{'alpha':>6} {'Reward':>12} {'PnL':>12} {'Conv':>10}")
|
|
||||||
print("-" * 45)
|
|
||||||
for alpha, m in sorted(results.items()):
|
|
||||||
print(f"{alpha:6.2f} {m['total_reward']:12.2f} {m['total_pnl']:12.2f} {m['avg_conversion']:10.3f}")
|
|
||||||
|
|
||||||
rewards = [results[a]['total_reward'] for a in sorted(results.keys())]
|
|
||||||
dataset = np.array([[a, r] for a, r in zip(alpha_values, rewards)])
|
|
||||||
trend = np.corrcoef(dataset[:, 0], dataset[:, 1])[0, 1]
|
|
||||||
print(f"Trend (alpha~reward correlation): {trend:.3f}")
|
|
||||||
|
|
||||||
|
|
||||||
def demo_policy_comparison():
|
|
||||||
print("\n" + "=" * 70)
|
|
||||||
print("EXPERIMENT: Policy Comparison under Contamination")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
cfg = ThesisConfig(n_instruments=5, alpha_contamination=0.25, max_steps=100, seed=42)
|
|
||||||
platform = make_thesis_platform(cfg)
|
|
||||||
|
|
||||||
def fixed_policy(obs, t): return platform.instruments.refs.copy(), 1.0
|
|
||||||
def aggressive_policy(obs, t): return platform.instruments.refs * 1.3, 1.0
|
|
||||||
def conservative_policy(obs, t): return platform.instruments.refs * 1.05, 1.0
|
|
||||||
def adaptive_policy(obs, t):
|
|
||||||
fills = obs[platform.instruments.n:2*platform.instruments.n]
|
|
||||||
exp = obs[2*platform.instruments.n:3*platform.instruments.n]
|
|
||||||
conv = np.sum(fills) / (np.sum(exp) + 1e-8)
|
|
||||||
return platform.instruments.refs * (1.0 + 0.2 * conv), 1.0
|
|
||||||
|
|
||||||
policies = {'fixed': fixed_policy, 'aggressive': aggressive_policy,
|
|
||||||
'conservative': conservative_policy, 'adaptive': adaptive_policy}
|
|
||||||
results = compare_policies(platform, policies, n_steps=100, n_runs=3, seed=42)
|
|
||||||
|
|
||||||
print(f"\n{'Policy':>15} {'Reward':>12} {'Std':>10} {'PnL':>12} {'Conv':>10}")
|
|
||||||
print("-" * 65)
|
|
||||||
for name, r in sorted(results.items(), key=lambda x: -x[1]['mean_reward']):
|
|
||||||
print(f"{name:>15} {r['mean_reward']:12.2f} {r['std_reward']:10.2f} "
|
|
||||||
f"{r['mean_pnl']:12.2f} {r['mean_conversion']:10.3f}")
|
|
||||||
|
|
||||||
|
|
||||||
def demo_session_analysis():
|
|
||||||
"""Analyze session-level behavior from MDP trajectories."""
|
|
||||||
print("\n" + "=" * 70)
|
|
||||||
print("EXPERIMENT: Session Analysis (Ground Truth)")
|
|
||||||
print("=" * 70)
|
|
||||||
|
|
||||||
from lab.outlet.constants import LogLevel
|
|
||||||
cfg = ThesisConfig(n_instruments=5, alpha_contamination=0.3, max_steps=50,
|
|
||||||
log_level=LogLevel.FULL, seed=42, use_real_behavior=True)
|
|
||||||
platform = make_thesis_platform(cfg)
|
|
||||||
|
|
||||||
result = platform.reset(seed=42)
|
|
||||||
human_sessions, agent_sessions = 0, 0
|
|
||||||
|
|
||||||
for t in range(cfg.max_steps):
|
|
||||||
action = platform.instruments.refs * 1.1
|
|
||||||
result = platform.step(action)
|
|
||||||
sep = compute_separability(result.logs, result.hidden.contamination)
|
|
||||||
human_sessions += sep.n_human_sessions
|
|
||||||
agent_sessions += sep.n_agent_sessions
|
|
||||||
|
|
||||||
total = human_sessions + agent_sessions
|
|
||||||
print(f"\nTotal sessions: {total}")
|
|
||||||
print(f"Human sessions: {human_sessions} ({100*human_sessions/total:.1f}%)")
|
|
||||||
print(f"Agent sessions: {agent_sessions} ({100*agent_sessions/total:.1f}%)")
|
|
||||||
print(f"True contamination: {cfg.alpha_contamination:.1%}")
|
|
||||||
print(f"Observed contamination: {agent_sessions/total:.1%}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
demo_basic_simulation()
|
|
||||||
demo_contamination_sweep()
|
|
||||||
# demo_policy_comparison()
|
|
||||||
# demo_session_analysis()
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
"""Behavioral separability for thesis human/agent classification.
|
|
||||||
|
|
||||||
Implements KL-divergence based separability scoring (Eq 20-21):
|
|
||||||
- Δ_H = D_KL(T̂' || T̄_H): divergence from human reference kernel
|
|
||||||
- Δ_A = D_KL(T̂' || T̄_A): divergence from agent reference kernel
|
|
||||||
- α̂(τ') = σ(β(Δ_H - Δ_A)): per-session contamination estimate
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from typing import Dict, List, TYPE_CHECKING
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .simplified import Session
|
|
||||||
|
|
||||||
|
|
||||||
# Reference transition kernels T̄_H, T̄_A estimated from real data (Eq 19)
|
|
||||||
TRANS_H = {
|
|
||||||
"start": {"view": 0.85, "end": 0.15},
|
|
||||||
"view": {"detail": 0.4, "add_to_cart": 0.3, "view": 0.2, "end": 0.1},
|
|
||||||
"detail": {"add_to_cart": 0.5, "view": 0.3, "end": 0.2},
|
|
||||||
"add_to_cart": {"purchase": 0.6, "view": 0.25, "end": 0.15},
|
|
||||||
"purchase": {"end": 1.0},
|
|
||||||
"checkout": {"purchase": 0.8, "end": 0.2},
|
|
||||||
"hover": {"view": 0.5, "detail": 0.3, "end": 0.2},
|
|
||||||
}
|
|
||||||
|
|
||||||
TRANS_A = {
|
|
||||||
"start": {"view": 0.95, "end": 0.05},
|
|
||||||
"view": {"detail": 0.6, "view": 0.25, "add_to_cart": 0.1, "end": 0.05},
|
|
||||||
"detail": {"view": 0.5, "add_to_cart": 0.15, "detail": 0.3, "end": 0.05},
|
|
||||||
"add_to_cart": {"view": 0.4, "purchase": 0.2, "end": 0.4},
|
|
||||||
"purchase": {"end": 1.0},
|
|
||||||
"checkout": {"purchase": 0.3, "end": 0.7},
|
|
||||||
"hover": {"view": 0.6, "detail": 0.35, "end": 0.05},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def kl_div(p: Dict[str, float], q: Dict[str, float], eps: float = 1e-10) -> float:
|
|
||||||
"""Compute KL(p || q) with smoothing."""
|
|
||||||
if not p or not q:
|
|
||||||
return 0.0
|
|
||||||
all_keys = set(p.keys()) | set(q.keys())
|
|
||||||
total = 0.0
|
|
||||||
for k in all_keys:
|
|
||||||
pk = p.get(k, eps)
|
|
||||||
qk = q.get(k, eps)
|
|
||||||
if pk > eps:
|
|
||||||
total += pk * np.log(pk / max(qk, eps))
|
|
||||||
return max(0.0, total)
|
|
||||||
|
|
||||||
|
|
||||||
def build_kernel(events: List) -> Dict[str, Dict[str, float]]:
|
|
||||||
"""Build empirical transition kernel from event sequence."""
|
|
||||||
trans: Dict[str, Dict[str, int]] = {}
|
|
||||||
prev = "start"
|
|
||||||
for e in events:
|
|
||||||
curr = getattr(e, 'action', None) or e.get('action', 'end') if isinstance(e, dict) else 'end'
|
|
||||||
trans.setdefault(prev, {})
|
|
||||||
trans[prev][curr] = trans[prev].get(curr, 0) + 1
|
|
||||||
prev = curr
|
|
||||||
# add terminal transition
|
|
||||||
trans.setdefault(prev, {})
|
|
||||||
trans[prev]["end"] = trans[prev].get("end", 0) + 1
|
|
||||||
|
|
||||||
# normalize to probabilities
|
|
||||||
kernel = {}
|
|
||||||
for s, dests in trans.items():
|
|
||||||
total = sum(dests.values())
|
|
||||||
kernel[s] = {d: c / total for d, c in dests.items()} if total > 0 else {"end": 1.0}
|
|
||||||
return kernel
|
|
||||||
|
|
||||||
|
|
||||||
def compute_divergence(kernel: Dict[str, Dict[str, float]], ref_h: Dict = None, ref_a: Dict = None) -> tuple[float, float]:
|
|
||||||
"""Compute Δ_H, Δ_A divergence from reference kernels (Eq 20-21)."""
|
|
||||||
ref_h = ref_h or TRANS_H
|
|
||||||
ref_a = ref_a or TRANS_A
|
|
||||||
delta_h = sum(kl_div(kernel.get(s, {}), ref_h.get(s, {})) for s in kernel) / max(len(kernel), 1)
|
|
||||||
delta_a = sum(kl_div(kernel.get(s, {}), ref_a.get(s, {})) for s in kernel) / max(len(kernel), 1)
|
|
||||||
return delta_h, delta_a
|
|
||||||
|
|
||||||
|
|
||||||
def estimate_alpha(session: "Session", beta: float = 2.0) -> float:
|
|
||||||
"""Estimate per-session contamination α̂(τ') = σ(β(Δ_H - Δ_A)).
|
|
||||||
|
|
||||||
High Δ_H (far from human) and low Δ_A (close to agent) -> high α̂ (likely agent).
|
|
||||||
"""
|
|
||||||
if not session.events:
|
|
||||||
return 0.5
|
|
||||||
kernel = build_kernel(session.events)
|
|
||||||
delta_h, delta_a = compute_divergence(kernel)
|
|
||||||
|
|
||||||
if delta_h + delta_a < 1e-6:
|
|
||||||
return 0.5
|
|
||||||
|
|
||||||
# sigmoid: high when trajectory is more divergent from human than agent
|
|
||||||
return 1.0 / (1.0 + np.exp(-beta * (delta_h - delta_a)))
|
|
||||||
|
|
||||||
|
|
||||||
def batch_estimate_alpha(sessions: List["Session"]) -> tuple[float, List[float]]:
|
|
||||||
"""Estimate aggregate and per-session contamination."""
|
|
||||||
if not sessions:
|
|
||||||
return 0.0, []
|
|
||||||
alphas = [estimate_alpha(s) for s in sessions]
|
|
||||||
return float(np.mean(alphas)), alphas
|
|
||||||
156
lab/config.py
156
lab/config.py
@@ -1,156 +0,0 @@
|
|||||||
"""
|
|
||||||
Configuration and factory functions for creating pre-configured platforms.
|
|
||||||
|
|
||||||
This module provides:
|
|
||||||
- RetailConfig, MarketMakingConfig: Configuration dataclasses
|
|
||||||
- make_retail_platform: Factory for retail dynamic pricing scenarios
|
|
||||||
- make_market_making_platform: Factory for market making scenarios
|
|
||||||
|
|
||||||
Example:
|
|
||||||
>>> from lab.config import make_retail_platform
|
|
||||||
>>> platform = make_retail_platform(RetailConfig(n_instruments=5))
|
|
||||||
>>> result = platform.reset(seed=42)
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import numpy as np
|
|
||||||
from .outlet import (Platform, PlatformConfig, PositionModel, PositionConfig,
|
|
||||||
PostedPriceMechanism, TwoSidedMechanism, make_instruments,
|
|
||||||
InstrumentType, LogLevel)
|
|
||||||
from .outlet.mechanisms.posted_price import PostedPriceConfig
|
|
||||||
from .outlet.mechanisms.two_sided import TwoSidedConfig
|
|
||||||
from .population import (SessionArrivalModel, PoissonArrivalModel, HawkesArrivalModel,
|
|
||||||
ElasticityExecutionModel, IntensityExecutionModel,
|
|
||||||
ReactiveCompetitorModel, GBMMarketModel)
|
|
||||||
from .population.arrivals import SessionArrivalConfig, PoissonArrivalConfig, HawkesArrivalConfig
|
|
||||||
from .population.execution import ElasticityConfig, IntensityConfig
|
|
||||||
from .population.competitors import ReactiveCompetitorConfig, GBMMarketConfig
|
|
||||||
from .outlet.objectives.factory import retail_objective, market_making_objective
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RetailConfig:
|
|
||||||
"""Configuration for retail dynamic pricing scenario.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
n_instruments: Number of products to price
|
|
||||||
cost_range: (min, max) for random product costs
|
|
||||||
margin_range: (min, max) for random initial margins
|
|
||||||
initial_inventory: Starting inventory per product
|
|
||||||
holding_cost_rate: Cost per unit per step for holding
|
|
||||||
sessions_per_step: Number of browsing sessions per step
|
|
||||||
contamination: Fraction of sessions that are scrapers
|
|
||||||
max_steps: Maximum episode length
|
|
||||||
seed: Random seed for reproducibility
|
|
||||||
"""
|
|
||||||
n_instruments: int = 10
|
|
||||||
cost_range: tuple[float, float] = (5.0, 50.0)
|
|
||||||
margin_range: tuple[float, float] = (0.2, 0.5)
|
|
||||||
initial_inventory: float = 100.0
|
|
||||||
holding_cost_rate: float = 0.002
|
|
||||||
sessions_per_step: int = 30
|
|
||||||
contamination: float = 0.1
|
|
||||||
max_steps: int = 500
|
|
||||||
seed: int | None = None
|
|
||||||
|
|
||||||
def make_retail_platform(cfg: RetailConfig | None = None) -> Platform:
|
|
||||||
"""Create a pre-configured retail dynamic pricing platform.
|
|
||||||
|
|
||||||
Components:
|
|
||||||
- Mechanism: PostedPriceMechanism (single price per product)
|
|
||||||
- Arrivals: SessionArrivalModel (browsing sessions with views)
|
|
||||||
- Execution: ElasticityExecutionModel (price sensitivity)
|
|
||||||
- Market: ReactiveCompetitorModel (can trigger price wars)
|
|
||||||
- Objective: PnL - holding_cost - volatility - lost_opportunity
|
|
||||||
|
|
||||||
Args:
|
|
||||||
cfg: Configuration (uses defaults if None)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Configured Platform instance
|
|
||||||
"""
|
|
||||||
cfg = cfg or RetailConfig()
|
|
||||||
rng = np.random.default_rng(cfg.seed)
|
|
||||||
|
|
||||||
instruments = make_instruments(cfg.n_instruments, cfg.cost_range, cfg.margin_range,
|
|
||||||
InstrumentType.SKU, rng)
|
|
||||||
instruments.position = np.full(cfg.n_instruments, cfg.initial_inventory)
|
|
||||||
|
|
||||||
mechanism = PostedPriceMechanism(PostedPriceConfig())
|
|
||||||
arrival = SessionArrivalModel(SessionArrivalConfig(
|
|
||||||
sessions_per_step=cfg.sessions_per_step, contamination=cfg.contamination))
|
|
||||||
execution = ElasticityExecutionModel(ElasticityConfig())
|
|
||||||
position = PositionModel(PositionConfig(
|
|
||||||
initial_position=cfg.initial_inventory,
|
|
||||||
holding_cost_rate=cfg.holding_cost_rate))
|
|
||||||
market = ReactiveCompetitorModel(ReactiveCompetitorConfig(), refs=instruments.refs)
|
|
||||||
objective = retail_objective()
|
|
||||||
|
|
||||||
return Platform(
|
|
||||||
instruments=instruments, mechanism=mechanism, arrival=arrival,
|
|
||||||
execution=execution, position=position, market=market, objective=objective,
|
|
||||||
cfg=PlatformConfig(n_instruments=cfg.n_instruments, max_steps=cfg.max_steps,
|
|
||||||
seed=cfg.seed, log_level=LogLevel.AGG_ONLY)
|
|
||||||
)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MarketMakingConfig:
|
|
||||||
"""Configuration for market making scenario.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
n_instruments: Number of assets to quote
|
|
||||||
initial_mid: Initial mid-price for assets
|
|
||||||
mu: Price drift (expected return)
|
|
||||||
sigma: Price volatility
|
|
||||||
gamma: Inventory risk aversion parameter
|
|
||||||
base_arrival_rate: Order arrival rate (Hawkes baseline)
|
|
||||||
max_steps: Maximum episode length
|
|
||||||
seed: Random seed for reproducibility
|
|
||||||
"""
|
|
||||||
n_instruments: int = 5
|
|
||||||
initial_mid: float = 100.0
|
|
||||||
mu: float = 0.0
|
|
||||||
sigma: float = 0.02
|
|
||||||
gamma: float = 0.1
|
|
||||||
base_arrival_rate: float = 20.0
|
|
||||||
max_steps: int = 1000
|
|
||||||
seed: int | None = None
|
|
||||||
|
|
||||||
def make_market_making_platform(cfg: MarketMakingConfig | None = None) -> Platform:
|
|
||||||
"""Create a pre-configured market making platform.
|
|
||||||
|
|
||||||
Components:
|
|
||||||
- Mechanism: TwoSidedMechanism (bid-ask spread quoting)
|
|
||||||
- Arrivals: HawkesArrivalModel (clustered order flow)
|
|
||||||
- Execution: IntensityExecutionModel (distance-based fills)
|
|
||||||
- Market: GBMMarketModel (geometric Brownian motion mid-prices)
|
|
||||||
- Objective: PnL + spread_capture - inventory_risk
|
|
||||||
|
|
||||||
Args:
|
|
||||||
cfg: Configuration (uses defaults if None)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Configured Platform instance
|
|
||||||
"""
|
|
||||||
cfg = cfg or MarketMakingConfig()
|
|
||||||
rng = np.random.default_rng(cfg.seed)
|
|
||||||
|
|
||||||
instruments = make_instruments(cfg.n_instruments, (cfg.initial_mid*0.9, cfg.initial_mid*1.1),
|
|
||||||
(0.0, 0.0), InstrumentType.ASSET, rng)
|
|
||||||
instruments.position = np.zeros(cfg.n_instruments)
|
|
||||||
|
|
||||||
mechanism = TwoSidedMechanism(TwoSidedConfig())
|
|
||||||
arrival = HawkesArrivalModel(HawkesArrivalConfig(base_rate=cfg.base_arrival_rate))
|
|
||||||
execution = IntensityExecutionModel(IntensityConfig())
|
|
||||||
position = PositionModel(PositionConfig(
|
|
||||||
initial_position=0.0, min_position=-500, max_position=500,
|
|
||||||
holding_cost_rate=0.0)) # use inventory risk penalty instead
|
|
||||||
market = GBMMarketModel(GBMMarketConfig(mu=cfg.mu, sigma=cfg.sigma),
|
|
||||||
initial=instruments.refs)
|
|
||||||
objective = market_making_objective(gamma=cfg.gamma, sigma=cfg.sigma)
|
|
||||||
|
|
||||||
return Platform(
|
|
||||||
instruments=instruments, mechanism=mechanism, arrival=arrival,
|
|
||||||
execution=execution, position=position, market=market, objective=objective,
|
|
||||||
cfg=PlatformConfig(n_instruments=cfg.n_instruments, max_steps=cfg.max_steps,
|
|
||||||
seed=cfg.seed, log_level=LogLevel.AGG_ONLY)
|
|
||||||
)
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
SPHINXOPTS ?=
|
|
||||||
SPHINXBUILD ?= sphinx-build
|
|
||||||
SOURCEDIR = .
|
|
||||||
BUILDDIR = _build
|
|
||||||
|
|
||||||
help:
|
|
||||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
||||||
|
|
||||||
.PHONY: help Makefile
|
|
||||||
|
|
||||||
%: Makefile
|
|
||||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import os
|
|
||||||
import sys
|
|
||||||
sys.path.insert(0, os.path.abspath('../..'))
|
|
||||||
|
|
||||||
project = 'Quote-Control Simulator'
|
|
||||||
copyright = '2025, PHANTOM Research'
|
|
||||||
author = 'PHANTOM Research'
|
|
||||||
release = '0.1.0'
|
|
||||||
|
|
||||||
extensions = [
|
|
||||||
'sphinx.ext.autodoc',
|
|
||||||
'sphinx.ext.napoleon',
|
|
||||||
'sphinx.ext.viewcode',
|
|
||||||
'sphinx.ext.intersphinx',
|
|
||||||
'sphinx.ext.autosummary',
|
|
||||||
]
|
|
||||||
|
|
||||||
templates_path = ['_templates']
|
|
||||||
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
|
||||||
|
|
||||||
html_theme = 'alabaster'
|
|
||||||
html_static_path = ['_static']
|
|
||||||
|
|
||||||
autodoc_default_options = {
|
|
||||||
'members': True,
|
|
||||||
'undoc-members': True,
|
|
||||||
'show-inheritance': True,
|
|
||||||
}
|
|
||||||
|
|
||||||
napoleon_google_docstring = True
|
|
||||||
napoleon_numpy_docstring = True
|
|
||||||
napoleon_include_init_with_doc = True
|
|
||||||
|
|
||||||
intersphinx_mapping = {
|
|
||||||
'python': ('https://docs.python.org/3', None),
|
|
||||||
'numpy': ('https://numpy.org/doc/stable/', None),
|
|
||||||
}
|
|
||||||
|
|
||||||
autosummary_generate = True
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
Quote-Control Simulator
|
|
||||||
=======================
|
|
||||||
|
|
||||||
Research-grade platform for dynamic pricing and market making experiments.
|
|
||||||
|
|
||||||
The platform abstracts pricing as: **Quote → Arrival → Execution → Position**
|
|
||||||
|
|
||||||
Supports multiple mechanisms:
|
|
||||||
|
|
||||||
* **PostedPrice**: retail dynamic pricing
|
|
||||||
* **TwoSided**: market making with bid-ask spreads
|
|
||||||
* **Auction**: reserve/shading for auction settings
|
|
||||||
|
|
||||||
Quick Start
|
|
||||||
-----------
|
|
||||||
|
|
||||||
.. code-block:: python
|
|
||||||
|
|
||||||
from lab.config import make_retail_platform
|
|
||||||
from lab.experiments import rollout, fixed_price_policy
|
|
||||||
|
|
||||||
platform = make_retail_platform()
|
|
||||||
policy = fixed_price_policy(platform.instruments.refs)
|
|
||||||
result = rollout(platform, policy, n_steps=100)
|
|
||||||
print(f"Total PnL: {result.total_pnl:.2f}")
|
|
||||||
|
|
||||||
.. toctree::
|
|
||||||
:maxdepth: 2
|
|
||||||
:caption: Contents:
|
|
||||||
|
|
||||||
system_overview
|
|
||||||
modules/outlet
|
|
||||||
modules/population
|
|
||||||
modules/experiments
|
|
||||||
|
|
||||||
Indices
|
|
||||||
-------
|
|
||||||
|
|
||||||
* :ref:`genindex`
|
|
||||||
* :ref:`modindex`
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
Experiments
|
|
||||||
===========
|
|
||||||
|
|
||||||
Evaluation & OPE
|
|
||||||
----------------
|
|
||||||
|
|
||||||
.. automodule:: lab.experiments.eval
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Configuration
|
|
||||||
-------------
|
|
||||||
|
|
||||||
.. automodule:: lab.config
|
|
||||||
:members:
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
Outlet (Core Simulator)
|
|
||||||
=======================
|
|
||||||
|
|
||||||
Types
|
|
||||||
-----
|
|
||||||
|
|
||||||
.. automodule:: lab.outlet.types
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Constants
|
|
||||||
---------
|
|
||||||
|
|
||||||
.. automodule:: lab.outlet.constants
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Protocols
|
|
||||||
---------
|
|
||||||
|
|
||||||
.. automodule:: lab.outlet.protocols
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Platform
|
|
||||||
--------
|
|
||||||
|
|
||||||
.. automodule:: lab.outlet.platform
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Stock & Position
|
|
||||||
----------------
|
|
||||||
|
|
||||||
.. automodule:: lab.outlet.stock
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Observation
|
|
||||||
-----------
|
|
||||||
|
|
||||||
.. automodule:: lab.outlet.observation
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Mechanisms
|
|
||||||
----------
|
|
||||||
|
|
||||||
Posted Price
|
|
||||||
~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. automodule:: lab.outlet.mechanisms.posted_price
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Two-Sided (Market Making)
|
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
||||||
|
|
||||||
.. automodule:: lab.outlet.mechanisms.two_sided
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Auction
|
|
||||||
~~~~~~~
|
|
||||||
|
|
||||||
.. automodule:: lab.outlet.mechanisms.auction
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Objectives
|
|
||||||
----------
|
|
||||||
|
|
||||||
.. automodule:: lab.outlet.objectives.base
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. automodule:: lab.outlet.objectives.penalties
|
|
||||||
:members:
|
|
||||||
|
|
||||||
.. automodule:: lab.outlet.objectives.factory
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Math Utilities
|
|
||||||
--------------
|
|
||||||
|
|
||||||
.. automodule:: lab.outlet.math_util
|
|
||||||
:members:
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
Population Models
|
|
||||||
=================
|
|
||||||
|
|
||||||
Arrival Models
|
|
||||||
--------------
|
|
||||||
|
|
||||||
.. automodule:: lab.population.arrivals
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Execution Models
|
|
||||||
----------------
|
|
||||||
|
|
||||||
.. automodule:: lab.population.execution
|
|
||||||
:members:
|
|
||||||
|
|
||||||
Competitor / Market Models
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
.. automodule:: lab.population.competitors
|
|
||||||
:members:
|
|
||||||
@@ -1,97 +0,0 @@
|
|||||||
System Overview
|
|
||||||
===============
|
|
||||||
|
|
||||||
The simulator organises dynamic pricing and market-making experiments as a
|
|
||||||
closed loop with the following stages:
|
|
||||||
|
|
||||||
* **Quote** – a policy or agent emits a :class:`lab.outlet.types.Quote`. The
|
|
||||||
quote is normalised and validated by a concrete
|
|
||||||
:class:`lab.outlet.protocols.Mechanism` implementation
|
|
||||||
(posted-price, two-sided, auction).
|
|
||||||
* **Arrival** – a :class:`lab.outlet.protocols.ArrivalModel` samples a stream of
|
|
||||||
:class:`lab.outlet.types.Opportunity` objects given the current time,
|
|
||||||
instrument catalogue, and market state.
|
|
||||||
* **Execution** – the :class:`lab.outlet.protocols.ExecutionModel` converts an
|
|
||||||
opportunity into a probabilistic fill using the active quote, optional
|
|
||||||
competitor prices, and demand-side context.
|
|
||||||
* **Position** – a :class:`lab.outlet.protocols.PositionModel` enforces
|
|
||||||
inventory or position constraints, censors oversized fills, and accrues
|
|
||||||
holding and shortage costs.
|
|
||||||
* **Observation & Reward** – the
|
|
||||||
:class:`lab.outlet.protocols.ObservationBuilder` constructs the censored view
|
|
||||||
exposed to the agent, while a :class:`lab.outlet.protocols.Objective`
|
|
||||||
transforms :class:`lab.outlet.types.StepMetrics` into a scalar reward with an
|
|
||||||
optional breakdown per term.
|
|
||||||
|
|
||||||
These components are orchestrated by :class:`lab.outlet.platform.Platform`,
|
|
||||||
which manages internal hidden state, deterministic seeding, and logging.
|
|
||||||
|
|
||||||
Component Matrix
|
|
||||||
----------------
|
|
||||||
|
|
||||||
=============================== ==============================================
|
|
||||||
Layer Responsibilities / Examples
|
|
||||||
=============================== ==============================================
|
|
||||||
Mechanisms Quote normalisation, execution semantics
|
|
||||||
(`posted_price`, `two_sided`, `auction`).
|
|
||||||
Population models Arrivals (:mod:`lab.population.arrivals`),
|
|
||||||
execution probability models
|
|
||||||
(:mod:`lab.population.execution`), and
|
|
||||||
competitor or market dynamics
|
|
||||||
(:mod:`lab.population.competitors`).
|
|
||||||
Position management Inventory limits, replenishment, holding and
|
|
||||||
shortage costs (:mod:`lab.outlet.stock`).
|
|
||||||
Observation & logging Censored observations and optional event logs
|
|
||||||
(:mod:`lab.outlet.observation`).
|
|
||||||
Objectives Reward composition utilities
|
|
||||||
(:mod:`lab.outlet.objectives`).
|
|
||||||
Experiments Rollout helpers, baseline policies, off-policy
|
|
||||||
evaluation (:mod:`lab.experiments.eval`).
|
|
||||||
=============================== ==============================================
|
|
||||||
|
|
||||||
Preconfigured Platforms
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
Two high-level factories in :mod:`lab.config` wire common combinations of the
|
|
||||||
building blocks:
|
|
||||||
|
|
||||||
* **Retail dynamic pricing** – posted-price mechanism, session arrivals with
|
|
||||||
contamination, elasticity-based executions, reactive competitor model, and a
|
|
||||||
composite objective that penalises volatility, holding costs, and lost
|
|
||||||
opportunities.
|
|
||||||
* **Market making** – two-sided quoting, Hawkes order flow, intensity-based
|
|
||||||
executions, geometric Brownian motion mid-prices, and an objective combining
|
|
||||||
PnL, spread capture, and quadratic inventory risk.
|
|
||||||
|
|
||||||
State & Reset Behaviour
|
|
||||||
-----------------------
|
|
||||||
|
|
||||||
When you call :meth:`lab.outlet.platform.Platform.reset`, the platform resets
|
|
||||||
instrument positions, quotes, and hidden state, but component implementations
|
|
||||||
may maintain their own internal buffers. For reproducible experiments:
|
|
||||||
|
|
||||||
* Reuse freshly instantiated arrival/market models per episode, or add explicit
|
|
||||||
``reset`` methods if the model keeps history (for example,
|
|
||||||
:class:`lab.population.arrivals.HawkesArrivalModel` maintains an event
|
|
||||||
history, while :class:`lab.population.competitors.ReactiveCompetitorModel`
|
|
||||||
tracks prior competitor quotes).
|
|
||||||
* Seed randomness through the factory configuration (``RetailConfig.seed`` or
|
|
||||||
``MarketMakingConfig.seed``) or pass a seed to ``Platform.reset`` for
|
|
||||||
deterministic rollouts.
|
|
||||||
|
|
||||||
Extending the Platform
|
|
||||||
----------------------
|
|
||||||
|
|
||||||
To support a new domain:
|
|
||||||
|
|
||||||
1. Create custom Mechanism/Arrival/Execution/Market/Observation components by
|
|
||||||
implementing the respective protocol in :mod:`lab.outlet.protocols`.
|
|
||||||
2. Compose a new objective with
|
|
||||||
:func:`lab.outlet.objectives.factory.make_composite` or write a bespoke
|
|
||||||
:class:`lab.outlet.objectives.base.BaseObjective`.
|
|
||||||
3. Wire everything together via :class:`lab.outlet.platform.Platform` directly
|
|
||||||
or expose a helper factory in :mod:`lab.config`.
|
|
||||||
|
|
||||||
Use :func:`lab.experiments.rollout` and
|
|
||||||
:func:`lab.experiments.compare_policies` to benchmark candidate policies under
|
|
||||||
multiple random seeds, collecting per-step logs for analysis or OPE.
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from .eval import (rollout, RolloutResult, compare_policies, compute_ips, OPEResult,
|
|
||||||
fixed_price_policy, cost_plus_margin_policy, random_walk_policy, epsilon_greedy_policy)
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'rollout', 'RolloutResult', 'compare_policies', 'compute_ips', 'OPEResult',
|
|
||||||
'fixed_price_policy', 'cost_plus_margin_policy', 'random_walk_policy', 'epsilon_greedy_policy',
|
|
||||||
]
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
"""
|
|
||||||
Evaluation utilities for policy testing and off-policy evaluation.
|
|
||||||
|
|
||||||
This module provides:
|
|
||||||
- rollout: Run a policy on the platform for multiple steps
|
|
||||||
- compare_policies: Compare multiple policies with statistics
|
|
||||||
- Baseline policies: fixed_price, cost_plus_margin, random_walk, epsilon_greedy
|
|
||||||
- OPE estimators: IPS and SNIPS for off-policy evaluation
|
|
||||||
|
|
||||||
Example:
|
|
||||||
>>> from lab.config import make_retail_platform
|
|
||||||
>>> from lab.experiments.eval import rollout, fixed_price_policy
|
|
||||||
>>> platform = make_retail_platform()
|
|
||||||
>>> policy = fixed_price_policy(platform.instruments.refs)
|
|
||||||
>>> result = rollout(platform, policy, n_steps=100)
|
|
||||||
>>> print(f"Total PnL: {result.total_pnl:.2f}")
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Callable, Any
|
|
||||||
import numpy as np
|
|
||||||
from ..outlet.platform import Platform
|
|
||||||
from ..outlet.types import StepResult, StepLogs, Quote
|
|
||||||
|
|
||||||
# Policy signature: takes (observation_flat, timestep) -> (action_prices, propensity)
|
|
||||||
Policy = Callable[[np.ndarray, int], tuple[np.ndarray, float]]
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RolloutResult:
|
|
||||||
"""Results from a policy rollout.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
rewards: Per-step rewards
|
|
||||||
metrics: Per-step StepMetrics objects
|
|
||||||
logs: Per-step StepLogs objects
|
|
||||||
total_reward: Sum of rewards
|
|
||||||
total_pnl: Sum of PnL from metrics
|
|
||||||
avg_conversion: Average conversion rate
|
|
||||||
"""
|
|
||||||
rewards: list[float]
|
|
||||||
metrics: list[Any]
|
|
||||||
logs: list[StepLogs]
|
|
||||||
total_reward: float
|
|
||||||
total_pnl: float
|
|
||||||
avg_conversion: float
|
|
||||||
|
|
||||||
def rollout(platform: Platform, policy: Policy, n_steps: int, seed: int | None = None) -> RolloutResult:
|
|
||||||
"""Execute a policy on the platform for n_steps.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
platform: The simulation platform
|
|
||||||
policy: Function (obs, t) -> (action, propensity)
|
|
||||||
n_steps: Number of steps to run
|
|
||||||
seed: Random seed for reproducibility
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
RolloutResult with rewards, metrics, and summary statistics
|
|
||||||
"""
|
|
||||||
result = platform.reset(seed)
|
|
||||||
rewards, metrics, logs = [], [], []
|
|
||||||
|
|
||||||
for t in range(n_steps):
|
|
||||||
obs_flat = result.obs.to_flat()
|
|
||||||
action, propensity = policy(obs_flat, t)
|
|
||||||
result = platform.step(action, propensity)
|
|
||||||
rewards.append(result.reward)
|
|
||||||
metrics.append(result.metrics)
|
|
||||||
logs.append(result.logs)
|
|
||||||
if result.terminated or result.truncated:
|
|
||||||
break
|
|
||||||
|
|
||||||
return RolloutResult(
|
|
||||||
rewards=rewards, metrics=metrics, logs=logs,
|
|
||||||
total_reward=sum(rewards),
|
|
||||||
total_pnl=sum(m.pnl for m in metrics),
|
|
||||||
avg_conversion=np.mean([m.conversion for m in metrics])
|
|
||||||
)
|
|
||||||
|
|
||||||
# Baseline policies for comparison
|
|
||||||
|
|
||||||
def fixed_price_policy(refs: np.ndarray) -> Policy:
|
|
||||||
"""Policy that always quotes at reference prices."""
|
|
||||||
def policy(obs: np.ndarray, t: int) -> tuple[np.ndarray, float]:
|
|
||||||
return refs.copy(), 1.0
|
|
||||||
return policy
|
|
||||||
|
|
||||||
def cost_plus_margin_policy(costs: np.ndarray, margin: float = 0.3) -> Policy:
|
|
||||||
"""Policy that quotes at cost * (1 + margin)."""
|
|
||||||
prices = costs * (1 + margin)
|
|
||||||
def policy(obs: np.ndarray, t: int) -> tuple[np.ndarray, float]:
|
|
||||||
return prices.copy(), 1.0
|
|
||||||
return policy
|
|
||||||
|
|
||||||
def random_walk_policy(refs: np.ndarray, volatility: float = 0.05,
|
|
||||||
rng: np.random.Generator | None = None) -> Policy:
|
|
||||||
"""Policy that performs a random walk around reference prices."""
|
|
||||||
rng = rng or np.random.default_rng()
|
|
||||||
prices = refs.copy()
|
|
||||||
def policy(obs: np.ndarray, t: int) -> tuple[np.ndarray, float]:
|
|
||||||
nonlocal prices
|
|
||||||
delta = rng.normal(0, volatility, len(prices))
|
|
||||||
prices = prices * (1 + delta)
|
|
||||||
prices = np.clip(prices, refs * 0.5, refs * 2.0)
|
|
||||||
return prices.copy(), 1.0
|
|
||||||
return policy
|
|
||||||
|
|
||||||
def epsilon_greedy_policy(base_policy: Policy, refs: np.ndarray,
|
|
||||||
epsilon: float = 0.1, rng: np.random.Generator | None = None) -> Policy:
|
|
||||||
"""Wrap a policy with epsilon-greedy exploration."""
|
|
||||||
rng = rng or np.random.default_rng()
|
|
||||||
def policy(obs: np.ndarray, t: int) -> tuple[np.ndarray, float]:
|
|
||||||
if rng.random() < epsilon:
|
|
||||||
action = refs * rng.uniform(0.8, 1.2, len(refs))
|
|
||||||
return action, epsilon / len(refs)
|
|
||||||
else:
|
|
||||||
action, _ = base_policy(obs, t)
|
|
||||||
return action, 1 - epsilon
|
|
||||||
return policy
|
|
||||||
|
|
||||||
# Off-Policy Evaluation (OPE)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class OPEResult:
|
|
||||||
"""Results from off-policy evaluation.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
ips_estimate: Inverse Propensity Scoring estimate
|
|
||||||
snips_estimate: Self-normalized IPS estimate (more stable)
|
|
||||||
n_samples: Number of samples used
|
|
||||||
effective_samples: Effective sample size (accounts for variance)
|
|
||||||
"""
|
|
||||||
ips_estimate: float
|
|
||||||
snips_estimate: float
|
|
||||||
n_samples: int
|
|
||||||
effective_samples: float
|
|
||||||
|
|
||||||
def compute_ips(logs: list[StepLogs], rewards: list[float],
|
|
||||||
target_policy: Policy, behavior_propensities: list[float] | None = None) -> OPEResult:
|
|
||||||
"""Compute IPS and SNIPS estimators for off-policy evaluation.
|
|
||||||
|
|
||||||
Uses logged propensities to estimate expected reward under a target
|
|
||||||
policy from data collected under a behavior policy.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
logs: Step logs containing propensities
|
|
||||||
rewards: Observed rewards from behavior policy
|
|
||||||
target_policy: Policy to evaluate (not currently used, assumes deterministic)
|
|
||||||
behavior_propensities: Override propensities if not in logs
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
OPEResult with IPS, SNIPS estimates and sample statistics
|
|
||||||
"""
|
|
||||||
if behavior_propensities is None:
|
|
||||||
# extract from logs
|
|
||||||
behavior_propensities = []
|
|
||||||
for log in logs:
|
|
||||||
if log.executions:
|
|
||||||
avg_prop = np.mean([e.propensity for e in log.executions])
|
|
||||||
else:
|
|
||||||
avg_prop = 1.0
|
|
||||||
behavior_propensities.append(avg_prop)
|
|
||||||
|
|
||||||
# compute importance weights
|
|
||||||
weights = []
|
|
||||||
for i, (log, bp) in enumerate(zip(logs, behavior_propensities)):
|
|
||||||
# target propensity would need obs reconstruction - simplified here
|
|
||||||
tp = 1.0 # assume deterministic target
|
|
||||||
w = tp / (bp + 1e-8)
|
|
||||||
weights.append(w)
|
|
||||||
|
|
||||||
weights = np.array(weights)
|
|
||||||
rewards = np.array(rewards)
|
|
||||||
|
|
||||||
# IPS estimate
|
|
||||||
ips = np.sum(weights * rewards) / len(rewards)
|
|
||||||
|
|
||||||
# SNIPS (self-normalized)
|
|
||||||
snips = np.sum(weights * rewards) / (np.sum(weights) + 1e-8)
|
|
||||||
|
|
||||||
# effective sample size
|
|
||||||
ess = (np.sum(weights) ** 2) / (np.sum(weights ** 2) + 1e-8)
|
|
||||||
|
|
||||||
return OPEResult(ips_estimate=ips, snips_estimate=snips,
|
|
||||||
n_samples=len(rewards), effective_samples=ess)
|
|
||||||
|
|
||||||
def compare_policies(platform: Platform, policies: dict[str, Policy],
|
|
||||||
n_steps: int = 100, n_runs: int = 5, seed: int = 42) -> dict[str, dict]:
|
|
||||||
"""Compare multiple policies with statistical summary.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
platform: Simulation platform
|
|
||||||
policies: Dict mapping policy names to policy functions
|
|
||||||
n_steps: Steps per rollout
|
|
||||||
n_runs: Number of rollouts per policy (different seeds)
|
|
||||||
seed: Base random seed
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict mapping policy names to result dicts with mean/std statistics
|
|
||||||
"""
|
|
||||||
results = {}
|
|
||||||
for name, policy in policies.items():
|
|
||||||
run_results = []
|
|
||||||
for i in range(n_runs):
|
|
||||||
r = rollout(platform, policy, n_steps, seed=seed + i)
|
|
||||||
run_results.append(r)
|
|
||||||
|
|
||||||
results[name] = {
|
|
||||||
'mean_reward': np.mean([r.total_reward for r in run_results]),
|
|
||||||
'std_reward': np.std([r.total_reward for r in run_results]),
|
|
||||||
'mean_pnl': np.mean([r.total_pnl for r in run_results]),
|
|
||||||
'mean_conversion': np.mean([r.avg_conversion for r in run_results]),
|
|
||||||
}
|
|
||||||
return results
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
from .constants import Side, MechanismType, InstrumentType, OpportunityType, EventType, LogLevel
|
|
||||||
from .types import (Instrument, InstrumentSet, Quote, Opportunity, Execution,
|
|
||||||
StepEvent, StepLogs, StepMetrics, MarketState, HiddenState, Observation, StepResult)
|
|
||||||
from .stock import PositionModel, PositionConfig, make_instruments
|
|
||||||
from .platform import Platform, PlatformConfig
|
|
||||||
from .observation import DefaultObservationBuilder, ObservationConfig
|
|
||||||
from .mechanisms import PostedPriceMechanism, TwoSidedMechanism, AuctionMechanism
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'Side', 'MechanismType', 'InstrumentType', 'OpportunityType', 'EventType', 'LogLevel',
|
|
||||||
'Instrument', 'InstrumentSet', 'Quote', 'Opportunity', 'Execution',
|
|
||||||
'StepEvent', 'StepLogs', 'StepMetrics', 'MarketState', 'HiddenState', 'Observation', 'StepResult',
|
|
||||||
'PositionModel', 'PositionConfig', 'make_instruments',
|
|
||||||
'Platform', 'PlatformConfig',
|
|
||||||
'DefaultObservationBuilder', 'ObservationConfig',
|
|
||||||
'PostedPriceMechanism', 'TwoSidedMechanism', 'AuctionMechanism',
|
|
||||||
]
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
"""
|
|
||||||
Constants and enumerations for the Quote-Control simulator.
|
|
||||||
|
|
||||||
This module defines the core enums used throughout the platform to ensure
|
|
||||||
type safety and consistent semantics across different pricing mechanisms.
|
|
||||||
"""
|
|
||||||
from enum import Enum, auto
|
|
||||||
|
|
||||||
class Side(Enum):
|
|
||||||
"""Transaction side indicator.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
BUY: Buyer-initiated transaction (customer purchases, market buy order)
|
|
||||||
SELL: Seller-initiated transaction (market sell order, short sale)
|
|
||||||
"""
|
|
||||||
BUY = auto()
|
|
||||||
SELL = auto()
|
|
||||||
|
|
||||||
class MechanismType(Enum):
|
|
||||||
"""Pricing mechanism type defining how quotes translate to executions.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
POSTED_PRICE: Single posted price per instrument (retail dynamic pricing)
|
|
||||||
TWO_SIDED_QUOTE: Bid-ask spread quoting (market making, liquidity provision)
|
|
||||||
AUCTION: Reserve price or bid shading (ad auctions, marketplaces)
|
|
||||||
"""
|
|
||||||
POSTED_PRICE = auto()
|
|
||||||
TWO_SIDED_QUOTE = auto()
|
|
||||||
AUCTION = auto()
|
|
||||||
|
|
||||||
class InstrumentType(Enum):
|
|
||||||
"""Type of instrument being priced.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
SKU: Retail product with inventory constraints
|
|
||||||
ASSET: Financial instrument with position limits
|
|
||||||
LOAN: Credit product with interest rate pricing
|
|
||||||
SUBSCRIPTION: Recurring service with periodic fees
|
|
||||||
"""
|
|
||||||
SKU = auto()
|
|
||||||
ASSET = auto()
|
|
||||||
LOAN = auto()
|
|
||||||
SUBSCRIPTION = auto()
|
|
||||||
|
|
||||||
class OpportunityType(Enum):
|
|
||||||
"""Type of arrival opportunity.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
SESSION: Retail browsing session with potential purchase intent
|
|
||||||
MARKET_ORDER: Financial market order arrival (buy or sell)
|
|
||||||
REQUEST: Service or credit request requiring quote response
|
|
||||||
"""
|
|
||||||
SESSION = auto()
|
|
||||||
MARKET_ORDER = auto()
|
|
||||||
REQUEST = auto()
|
|
||||||
|
|
||||||
class EventType(Enum):
|
|
||||||
"""Type of logged event during simulation.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
ARRIVAL: New opportunity arrived in the system
|
|
||||||
EXPOSURE: Quote was shown to an arrival
|
|
||||||
EXECUTION: Transaction was executed
|
|
||||||
ABANDON: Opportunity abandoned without execution
|
|
||||||
CANCEL: Pending order was cancelled
|
|
||||||
"""
|
|
||||||
ARRIVAL = auto()
|
|
||||||
EXPOSURE = auto()
|
|
||||||
EXECUTION = auto()
|
|
||||||
ABANDON = auto()
|
|
||||||
CANCEL = auto()
|
|
||||||
|
|
||||||
class LogLevel(Enum):
|
|
||||||
"""Verbosity level for step logging.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
NONE: No logging, fastest execution
|
|
||||||
AGG_ONLY: Only aggregate statistics per step
|
|
||||||
FULL: Full event-level logging with propensities for OPE
|
|
||||||
"""
|
|
||||||
NONE = auto()
|
|
||||||
AGG_ONLY = auto()
|
|
||||||
FULL = auto()
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
"""
|
|
||||||
Gymnasium-compatible wrapper for the Quote-Control platform.
|
|
||||||
|
|
||||||
Provides a standard Gym interface for RL training:
|
|
||||||
- observation_space: Box space with flattened observation
|
|
||||||
- action_space: Box space with price multipliers [0.5, 2.0]
|
|
||||||
- reset(), step(), render(), close() methods
|
|
||||||
|
|
||||||
Example:
|
|
||||||
>>> from lab.config import make_retail_platform
|
|
||||||
>>> from lab.outlet.gym_wrapper import QuoteGymEnv
|
|
||||||
>>> env = QuoteGymEnv(make_retail_platform())
|
|
||||||
>>> obs, info = env.reset()
|
|
||||||
>>> obs, reward, done, truncated, info = env.step(env.action_space.sample())
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from typing import Any
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
try:
|
|
||||||
import gymnasium as gym
|
|
||||||
from gymnasium import spaces
|
|
||||||
HAS_GYM = True
|
|
||||||
except ImportError:
|
|
||||||
HAS_GYM = False
|
|
||||||
|
|
||||||
from .platform import Platform, PlatformConfig
|
|
||||||
from .types import Quote, InstrumentSet, StepResult
|
|
||||||
|
|
||||||
class QuoteGymEnv:
|
|
||||||
"""Gymnasium-compatible environment wrapper.
|
|
||||||
|
|
||||||
Wraps a Platform instance with standard Gym interface.
|
|
||||||
Actions are price multipliers in [0.5, 2.0] applied to reference prices.
|
|
||||||
Observations are flattened numpy arrays containing quotes, fills, exposures.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, platform: Platform):
|
|
||||||
if not HAS_GYM:
|
|
||||||
raise ImportError("gymnasium required for QuoteGymEnv")
|
|
||||||
self.platform = platform
|
|
||||||
self.n = platform.instruments.n
|
|
||||||
self._last_result: StepResult | None = None
|
|
||||||
|
|
||||||
# action space: price adjustments as multipliers [0.5, 2.0]
|
|
||||||
self.action_space = spaces.Box(low=0.5, high=2.0, shape=(self.n,), dtype=np.float32)
|
|
||||||
|
|
||||||
# observation space
|
|
||||||
obs_dim = self.n * 4 # quotes + fills + exposures + position
|
|
||||||
if platform.market:
|
|
||||||
obs_dim += self.n # competitor quotes
|
|
||||||
self.observation_space = spaces.Box(low=-np.inf, high=np.inf,
|
|
||||||
shape=(obs_dim,), dtype=np.float32)
|
|
||||||
|
|
||||||
def reset(self, seed: int | None = None, options: dict | None = None) -> tuple[np.ndarray, dict]:
|
|
||||||
result = self.platform.reset(seed)
|
|
||||||
self._last_result = result
|
|
||||||
return result.obs.to_flat().astype(np.float32), result.info
|
|
||||||
|
|
||||||
def step(self, action: np.ndarray) -> tuple[np.ndarray, float, bool, bool, dict]:
|
|
||||||
# convert action (multipliers) to absolute prices
|
|
||||||
refs = self.platform.instruments.refs
|
|
||||||
prices = refs * action
|
|
||||||
result = self.platform.step(prices)
|
|
||||||
self._last_result = result
|
|
||||||
return (result.obs.to_flat().astype(np.float32), result.reward,
|
|
||||||
result.terminated, result.truncated, result.info)
|
|
||||||
|
|
||||||
def render(self) -> None:
|
|
||||||
if self._last_result:
|
|
||||||
m = self._last_result.metrics
|
|
||||||
print(f"t={self.platform._t} pnl={m.pnl:.2f} units={m.units_traded:.0f} "
|
|
||||||
f"conv={m.conversion:.3f} vol={m.volatility:.3f}")
|
|
||||||
|
|
||||||
def close(self) -> None:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def make_env(platform: Platform) -> QuoteGymEnv:
|
|
||||||
return QuoteGymEnv(platform)
|
|
||||||
|
|
||||||
if HAS_GYM:
|
|
||||||
# register if gymnasium available
|
|
||||||
try:
|
|
||||||
gym.register(id='QuoteControl-v0', entry_point='outlet.gym_wrapper:QuoteGymEnv')
|
|
||||||
except:
|
|
||||||
pass # already registered or other issue
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
"""
|
|
||||||
Numerical utilities for stable computation.
|
|
||||||
|
|
||||||
This module provides numerically stable implementations of common operations:
|
|
||||||
- safe_exp, safe_log: Avoid overflow/underflow
|
|
||||||
- softmax: Numerically stable softmax
|
|
||||||
- sigmoid, clamp: Standard transformations
|
|
||||||
- intensity_decay: Avellaneda-Stoikov fill intensity
|
|
||||||
- inventory_penalty: Quadratic inventory risk
|
|
||||||
- poisson_arrivals, hawkes_intensity: Arrival process helpers
|
|
||||||
|
|
||||||
All functions accept both scalars and numpy arrays.
|
|
||||||
"""
|
|
||||||
import numpy as np
|
|
||||||
|
|
||||||
EPS = 1e-8 # small constant to avoid division by zero
|
|
||||||
MAX_EXP = 700.0 # maximum safe exponent to avoid overflow
|
|
||||||
|
|
||||||
def safe_exp(x: np.ndarray | float) -> np.ndarray | float:
|
|
||||||
return np.exp(np.clip(x, -MAX_EXP, MAX_EXP))
|
|
||||||
|
|
||||||
def safe_log(x: np.ndarray | float) -> np.ndarray | float:
|
|
||||||
return np.log(np.maximum(x, EPS))
|
|
||||||
|
|
||||||
def clamp(x: np.ndarray | float, lo: float, hi: float) -> np.ndarray | float:
|
|
||||||
return np.clip(x, lo, hi)
|
|
||||||
|
|
||||||
def sigmoid(x: np.ndarray | float) -> np.ndarray | float:
|
|
||||||
return 1.0 / (1.0 + safe_exp(-x))
|
|
||||||
|
|
||||||
def softmax(x: np.ndarray, axis: int = -1) -> np.ndarray:
|
|
||||||
x_max = np.max(x, axis=axis, keepdims=True)
|
|
||||||
exp_x = safe_exp(x - x_max)
|
|
||||||
return exp_x / (np.sum(exp_x, axis=axis, keepdims=True) + EPS)
|
|
||||||
|
|
||||||
def geometric_series(base: float, ratio: float, n: int) -> np.ndarray:
|
|
||||||
return base * (ratio ** np.arange(n))
|
|
||||||
|
|
||||||
def ema(old: float, new: float, alpha: float = 0.1) -> float:
|
|
||||||
return alpha * new + (1 - alpha) * old
|
|
||||||
|
|
||||||
def intensity_decay(distance: float, kappa: float = 1.0) -> float:
|
|
||||||
"""Avellaneda-Stoikov style fill intensity decay with quote distance"""
|
|
||||||
return safe_exp(-kappa * distance)
|
|
||||||
|
|
||||||
def inventory_penalty(q: float, gamma: float = 0.1, sigma: float = 1.0) -> float:
|
|
||||||
"""Quadratic inventory risk penalty"""
|
|
||||||
return gamma * sigma**2 * q**2 / 2
|
|
||||||
|
|
||||||
def poisson_arrivals(rate: float, dt: float, rng: np.random.Generator) -> int:
|
|
||||||
return rng.poisson(rate * dt)
|
|
||||||
|
|
||||||
def hawkes_intensity(base: float, history: np.ndarray, alpha: float, beta: float, t: float) -> float:
|
|
||||||
"""Self-exciting Hawkes process intensity"""
|
|
||||||
if len(history) == 0: return base
|
|
||||||
decays = safe_exp(-beta * (t - history[history < t]))
|
|
||||||
return base + alpha * np.sum(decays)
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
from .posted_price import PostedPriceMechanism
|
|
||||||
from .two_sided import TwoSidedMechanism
|
|
||||||
from .auction import AuctionMechanism
|
|
||||||
|
|
||||||
__all__ = ['PostedPriceMechanism', 'TwoSidedMechanism', 'AuctionMechanism']
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
"""
|
|
||||||
Auction mechanism for reserve pricing and bid shading.
|
|
||||||
|
|
||||||
In this mechanism, the agent sets reserve prices that affect
|
|
||||||
win probability and clearing prices. Used for ad auctions,
|
|
||||||
marketplace auctions, and similar settings.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import numpy as np
|
|
||||||
from ..types import Quote, Opportunity, Execution, InstrumentSet, MarketState
|
|
||||||
from ..constants import Side
|
|
||||||
from ..math_util import clamp, sigmoid
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class AuctionConfig:
|
|
||||||
"""Configuration for auction mechanism.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
min_reserve: Minimum reserve price
|
|
||||||
max_reserve: Maximum reserve price
|
|
||||||
base_win_prob: Baseline win probability at reference reserve
|
|
||||||
sensitivity: How much higher reserves reduce win probability
|
|
||||||
"""
|
|
||||||
min_reserve: float = 0.0
|
|
||||||
max_reserve: float = 100.0
|
|
||||||
base_win_prob: float = 0.3
|
|
||||||
sensitivity: float = 2.0
|
|
||||||
|
|
||||||
class AuctionMechanism:
|
|
||||||
"""Auction mechanism for reserve pricing.
|
|
||||||
|
|
||||||
The agent sets reserve prices that affect:
|
|
||||||
- Win probability: higher reserves reduce chance of winning
|
|
||||||
- Clearing price: bounded between reserve and simulated max bid
|
|
||||||
|
|
||||||
Win probability: base_prob * sigmoid(-sensitivity * (reserve - ref) / ref)
|
|
||||||
Clearing price: max(reserve, min(max_bid, reserve + random_increment))
|
|
||||||
|
|
||||||
Only BUY-side opportunities are processed (auction wins).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cfg: AuctionConfig | None = None):
|
|
||||||
self.cfg = cfg or AuctionConfig()
|
|
||||||
|
|
||||||
def apply_quote(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
rng: np.random.Generator) -> Quote:
|
|
||||||
reserves = clamp(quote.prices, self.cfg.min_reserve, self.cfg.max_reserve)
|
|
||||||
return Quote(prices=reserves, propensity=quote.propensity, metadata=quote.metadata)
|
|
||||||
|
|
||||||
def process_opportunity(self, opp: Opportunity, quote: Quote,
|
|
||||||
instruments: InstrumentSet, market: MarketState | None,
|
|
||||||
rng: np.random.Generator) -> Execution | None:
|
|
||||||
if opp.side != Side.BUY: return None
|
|
||||||
idx = int(opp.instrument_id)
|
|
||||||
reserve = float(quote.prices[idx])
|
|
||||||
ref = instruments.refs[idx]
|
|
||||||
|
|
||||||
# win probability decreases with higher reserve
|
|
||||||
relative_reserve = (reserve - ref) / (ref + 1e-8)
|
|
||||||
win_prob = self.cfg.base_win_prob * sigmoid(-self.cfg.sensitivity * relative_reserve)
|
|
||||||
|
|
||||||
if rng.random() > win_prob: return None
|
|
||||||
|
|
||||||
# clearing price is between reserve and some max bid (simulated)
|
|
||||||
max_bid = ref * (1 + rng.exponential(0.2))
|
|
||||||
clearing = max(reserve, min(max_bid, reserve + rng.exponential(0.1) * ref))
|
|
||||||
|
|
||||||
return Execution(
|
|
||||||
opportunity_id=opp.id, instrument_id=opp.instrument_id,
|
|
||||||
side=opp.side, size_requested=opp.size, size_filled=opp.size,
|
|
||||||
price=clearing, propensity=quote.propensity * win_prob, t=opp.t
|
|
||||||
)
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
"""
|
|
||||||
Posted price mechanism for retail dynamic pricing.
|
|
||||||
|
|
||||||
In this mechanism, the agent posts a single price per instrument.
|
|
||||||
Buyers decide whether to purchase based on the posted price.
|
|
||||||
This is the standard e-commerce dynamic pricing model.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import numpy as np
|
|
||||||
from ..types import Quote, Opportunity, Execution, InstrumentSet, MarketState
|
|
||||||
from ..constants import Side
|
|
||||||
from ..math_util import clamp
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PostedPriceConfig:
|
|
||||||
"""Configuration for posted price mechanism.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
min_price: Absolute minimum price
|
|
||||||
max_price: Absolute maximum price
|
|
||||||
max_delta_pct: Maximum price change per step as fraction of previous
|
|
||||||
min_margin_pct: Minimum margin over cost basis
|
|
||||||
round_to: Price rounding granularity (None = no rounding)
|
|
||||||
"""
|
|
||||||
min_price: float = 0.01
|
|
||||||
max_price: float = 1000.0
|
|
||||||
max_delta_pct: float = 0.2
|
|
||||||
min_margin_pct: float = 0.05
|
|
||||||
round_to: float | None = 0.01
|
|
||||||
|
|
||||||
class PostedPriceMechanism:
|
|
||||||
"""Posted price mechanism for retail dynamic pricing.
|
|
||||||
|
|
||||||
The agent posts a single price per product. Constraints enforced:
|
|
||||||
- Prices within [min_price, max_price]
|
|
||||||
- Margin at least min_margin_pct above cost
|
|
||||||
- Price changes limited to max_delta_pct per step
|
|
||||||
- Prices rounded to round_to granularity
|
|
||||||
|
|
||||||
Only BUY-side opportunities are processed (customers purchasing).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cfg: PostedPriceConfig | None = None):
|
|
||||||
self.cfg = cfg or PostedPriceConfig()
|
|
||||||
|
|
||||||
def apply_quote(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
rng: np.random.Generator) -> Quote:
|
|
||||||
prices = quote.prices.copy()
|
|
||||||
costs = instruments.costs
|
|
||||||
refs = instruments.refs
|
|
||||||
c = self.cfg
|
|
||||||
|
|
||||||
# enforce min margin
|
|
||||||
min_prices = costs * (1 + c.min_margin_pct)
|
|
||||||
prices = np.maximum(prices, min_prices)
|
|
||||||
|
|
||||||
# enforce absolute bounds
|
|
||||||
prices = clamp(prices, c.min_price, c.max_price)
|
|
||||||
|
|
||||||
# enforce max delta if we have history
|
|
||||||
if 'prev_prices' in quote.metadata:
|
|
||||||
prev = quote.metadata['prev_prices']
|
|
||||||
max_change = prev * c.max_delta_pct
|
|
||||||
prices = clamp(prices, prev - max_change, prev + max_change)
|
|
||||||
|
|
||||||
# round prices
|
|
||||||
if c.round_to:
|
|
||||||
prices = np.round(prices / c.round_to) * c.round_to
|
|
||||||
|
|
||||||
return Quote(prices=prices, propensity=quote.propensity,
|
|
||||||
metadata={**quote.metadata, 'prev_prices': prices})
|
|
||||||
|
|
||||||
def process_opportunity(self, opp: Opportunity, quote: Quote,
|
|
||||||
instruments: InstrumentSet, market: MarketState | None,
|
|
||||||
rng: np.random.Generator) -> Execution | None:
|
|
||||||
if opp.side != Side.BUY: return None # posted price is buy-only
|
|
||||||
idx = int(opp.instrument_id)
|
|
||||||
price = float(quote.prices[idx])
|
|
||||||
return Execution(
|
|
||||||
opportunity_id=opp.id, instrument_id=opp.instrument_id,
|
|
||||||
side=opp.side, size_requested=opp.size, size_filled=opp.size,
|
|
||||||
price=price, propensity=quote.propensity, t=opp.t
|
|
||||||
)
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
"""
|
|
||||||
Two-sided quoting mechanism for market making.
|
|
||||||
|
|
||||||
In this mechanism, the agent posts both bid and ask prices.
|
|
||||||
Execution depends on the distance from the market mid-price.
|
|
||||||
This models liquidity provision in financial markets.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import numpy as np
|
|
||||||
from ..types import Quote, Opportunity, Execution, InstrumentSet, MarketState
|
|
||||||
from ..constants import Side
|
|
||||||
from ..math_util import clamp, intensity_decay
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class TwoSidedConfig:
|
|
||||||
"""Configuration for two-sided quoting mechanism.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
min_spread: Minimum bid-ask spread
|
|
||||||
max_spread: Maximum bid-ask spread
|
|
||||||
min_price: Absolute minimum price
|
|
||||||
max_price: Absolute maximum price
|
|
||||||
fill_kappa: Intensity decay parameter (higher = faster decay with distance)
|
|
||||||
"""
|
|
||||||
min_spread: float = 0.01
|
|
||||||
max_spread: float = 0.5
|
|
||||||
min_price: float = 0.01
|
|
||||||
max_price: float = 10000.0
|
|
||||||
fill_kappa: float = 1.5
|
|
||||||
|
|
||||||
class TwoSidedMechanism:
|
|
||||||
"""Two-sided quoting mechanism for market making.
|
|
||||||
|
|
||||||
The agent posts bid (buy) and ask (sell) prices around a mid-point.
|
|
||||||
Fill probability decays exponentially with distance from mid-price,
|
|
||||||
following the Avellaneda-Stoikov intensity model.
|
|
||||||
|
|
||||||
Both BUY and SELL opportunities are processed:
|
|
||||||
- BUY: customer buys at agent's ask price
|
|
||||||
- SELL: customer sells at agent's bid price
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cfg: TwoSidedConfig | None = None):
|
|
||||||
self.cfg = cfg or TwoSidedConfig()
|
|
||||||
|
|
||||||
def apply_quote(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
rng: np.random.Generator) -> Quote:
|
|
||||||
prices = quote.prices.copy()
|
|
||||||
spreads = quote.spreads.copy() if quote.spreads is not None else np.full_like(prices, 0.02)
|
|
||||||
c = self.cfg
|
|
||||||
|
|
||||||
prices = clamp(prices, c.min_price, c.max_price)
|
|
||||||
spreads = clamp(spreads, c.min_spread, c.max_spread)
|
|
||||||
|
|
||||||
# ensure bids < asks
|
|
||||||
half_spread = spreads / 2
|
|
||||||
bids = prices - half_spread
|
|
||||||
asks = prices + half_spread
|
|
||||||
bids = np.maximum(bids, c.min_price)
|
|
||||||
asks = np.minimum(asks, c.max_price)
|
|
||||||
spreads = asks - bids
|
|
||||||
prices = (bids + asks) / 2
|
|
||||||
|
|
||||||
return Quote(prices=prices, spreads=spreads, propensity=quote.propensity,
|
|
||||||
metadata=quote.metadata)
|
|
||||||
|
|
||||||
def process_opportunity(self, opp: Opportunity, quote: Quote,
|
|
||||||
instruments: InstrumentSet, market: MarketState | None,
|
|
||||||
rng: np.random.Generator) -> Execution | None:
|
|
||||||
idx = int(opp.instrument_id)
|
|
||||||
mid = market.mid_prices[idx] if market and market.mid_prices is not None else quote.prices[idx]
|
|
||||||
|
|
||||||
if opp.side == Side.BUY:
|
|
||||||
price = float(quote.asks[idx]) if quote.asks is not None else float(quote.prices[idx])
|
|
||||||
distance = price - mid
|
|
||||||
else:
|
|
||||||
price = float(quote.bids[idx]) if quote.bids is not None else float(quote.prices[idx])
|
|
||||||
distance = mid - price
|
|
||||||
|
|
||||||
# probabilistic fill based on distance from mid
|
|
||||||
fill_prob = intensity_decay(abs(distance), self.cfg.fill_kappa)
|
|
||||||
if rng.random() > fill_prob: return None
|
|
||||||
|
|
||||||
return Execution(
|
|
||||||
opportunity_id=opp.id, instrument_id=opp.instrument_id,
|
|
||||||
side=opp.side, size_requested=opp.size, size_filled=opp.size,
|
|
||||||
price=price, propensity=quote.propensity * fill_prob, t=opp.t
|
|
||||||
)
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
from .base import BaseObjective, CompositeObjective
|
|
||||||
from .penalties import (PnLObjective, VolatilityPenalty, HoldingCostPenalty,
|
|
||||||
LostOpportunityCostPenalty, InventoryRiskPenalty, SpreadCaptureReward)
|
|
||||||
from .factory import make_objective, make_composite, retail_objective, market_making_objective
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'BaseObjective', 'CompositeObjective',
|
|
||||||
'PnLObjective', 'VolatilityPenalty', 'HoldingCostPenalty',
|
|
||||||
'LostOpportunityCostPenalty', 'InventoryRiskPenalty', 'SpreadCaptureReward',
|
|
||||||
'make_objective', 'make_composite', 'retail_objective', 'market_making_objective',
|
|
||||||
]
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
"""
|
|
||||||
Base classes for reward objectives.
|
|
||||||
|
|
||||||
Objectives compute scalar rewards from step metrics. The CompositeObjective
|
|
||||||
allows combining multiple objectives with weights for multi-objective optimization.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from ..types import Quote, InstrumentSet, StepMetrics, HiddenState, Observation
|
|
||||||
|
|
||||||
class BaseObjective(ABC):
|
|
||||||
"""Abstract base class for reward objectives.
|
|
||||||
|
|
||||||
Subclasses must implement reward() and breakdown() methods.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def reward(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> float: ...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def breakdown(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> dict[str, float]: ...
|
|
||||||
|
|
||||||
class CompositeObjective(BaseObjective):
|
|
||||||
"""Weighted sum of multiple objectives.
|
|
||||||
|
|
||||||
Allows combining multiple reward terms (e.g., PnL - holding_cost - volatility).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
objectives: List of (objective, weight) tuples
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, objectives: list[tuple[BaseObjective, float]]):
|
|
||||||
self.objectives = objectives
|
|
||||||
|
|
||||||
def reward(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> float:
|
|
||||||
return sum(w * obj.reward(quote, instruments, metrics, hidden, obs)
|
|
||||||
for obj, w in self.objectives)
|
|
||||||
|
|
||||||
def breakdown(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> dict[str, float]:
|
|
||||||
bd = {}
|
|
||||||
for obj, w in self.objectives:
|
|
||||||
for k, v in obj.breakdown(quote, instruments, metrics, hidden, obs).items():
|
|
||||||
bd[k] = w * v
|
|
||||||
return bd
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
"""
|
|
||||||
Factory functions for creating objectives.
|
|
||||||
|
|
||||||
Provides:
|
|
||||||
- make_objective: Create single objective by name
|
|
||||||
- make_composite: Create weighted combination of objectives
|
|
||||||
- retail_objective: Default objective for retail pricing
|
|
||||||
- market_making_objective: Default objective for market making
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from .base import BaseObjective, CompositeObjective
|
|
||||||
from .penalties import (PnLObjective, VolatilityPenalty, HoldingCostPenalty,
|
|
||||||
LostOpportunityCostPenalty, InventoryRiskPenalty, SpreadCaptureReward)
|
|
||||||
|
|
||||||
REGISTRY: dict[str, type[BaseObjective]] = {
|
|
||||||
'pnl': PnLObjective,
|
|
||||||
'volatility': VolatilityPenalty,
|
|
||||||
'holding_cost': HoldingCostPenalty,
|
|
||||||
'lost_opportunity': LostOpportunityCostPenalty,
|
|
||||||
'inventory_risk': InventoryRiskPenalty,
|
|
||||||
'spread_capture': SpreadCaptureReward,
|
|
||||||
}
|
|
||||||
|
|
||||||
def make_objective(name: str, **kwargs) -> BaseObjective:
|
|
||||||
"""Create an objective by name.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: Objective name (pnl, volatility, holding_cost, lost_opportunity,
|
|
||||||
inventory_risk, spread_capture)
|
|
||||||
**kwargs: Passed to objective constructor
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Instantiated objective
|
|
||||||
"""
|
|
||||||
if name not in REGISTRY:
|
|
||||||
raise ValueError(f"Unknown objective: {name}. Available: {list(REGISTRY.keys())}")
|
|
||||||
return REGISTRY[name](**kwargs)
|
|
||||||
|
|
||||||
def make_composite(spec: list[tuple[str, float, dict]] | dict[str, float]) -> CompositeObjective:
|
|
||||||
"""Create composite objective from specification.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
spec: Either:
|
|
||||||
- list of (name, weight, kwargs) tuples for full control
|
|
||||||
- dict of {name: weight} for simple cases
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
CompositeObjective with specified components
|
|
||||||
"""
|
|
||||||
objectives = []
|
|
||||||
if isinstance(spec, dict):
|
|
||||||
for name, weight in spec.items():
|
|
||||||
objectives.append((make_objective(name), weight))
|
|
||||||
else:
|
|
||||||
for name, weight, kwargs in spec:
|
|
||||||
objectives.append((make_objective(name, **kwargs), weight))
|
|
||||||
return CompositeObjective(objectives)
|
|
||||||
|
|
||||||
def retail_objective(volatility_weight: float = 0.1, holding_weight: float = 0.5,
|
|
||||||
stockout_weight: float = 0.3) -> CompositeObjective:
|
|
||||||
"""Default objective for retail dynamic pricing.
|
|
||||||
|
|
||||||
Reward = PnL - volatility_weight*volatility - holding_weight*holding_cost
|
|
||||||
- stockout_weight*lost_opportunity
|
|
||||||
"""
|
|
||||||
return make_composite({
|
|
||||||
'pnl': 1.0,
|
|
||||||
'volatility': volatility_weight,
|
|
||||||
'holding_cost': holding_weight,
|
|
||||||
'lost_opportunity': stockout_weight,
|
|
||||||
})
|
|
||||||
|
|
||||||
def market_making_objective(gamma: float = 0.1, sigma: float = 1.0) -> CompositeObjective:
|
|
||||||
"""Default objective for market making.
|
|
||||||
|
|
||||||
Reward = PnL + 0.5*spread_capture - inventory_risk(gamma, sigma)
|
|
||||||
"""
|
|
||||||
return CompositeObjective([
|
|
||||||
(PnLObjective(), 1.0),
|
|
||||||
(SpreadCaptureReward(), 0.5),
|
|
||||||
(InventoryRiskPenalty(gamma=gamma, sigma=sigma), 1.0),
|
|
||||||
])
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
"""
|
|
||||||
Standard objective components and penalties.
|
|
||||||
|
|
||||||
This module provides common reward terms:
|
|
||||||
- PnLObjective: Basic profit and loss
|
|
||||||
- VolatilityPenalty: Penalize price volatility for UX
|
|
||||||
- HoldingCostPenalty: Inventory holding cost
|
|
||||||
- LostOpportunityCostPenalty: Stockout/missed fill cost
|
|
||||||
- InventoryRiskPenalty: Quadratic inventory risk (market making)
|
|
||||||
- SpreadCaptureReward: Bid-ask spread capture (market making)
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
import numpy as np
|
|
||||||
from .base import BaseObjective
|
|
||||||
from ..types import Quote, InstrumentSet, StepMetrics, HiddenState, Observation
|
|
||||||
from ..math_util import inventory_penalty
|
|
||||||
|
|
||||||
class PnLObjective(BaseObjective):
|
|
||||||
"""Profit and loss reward (revenue - cost)."""
|
|
||||||
|
|
||||||
def reward(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> float:
|
|
||||||
return metrics.pnl
|
|
||||||
|
|
||||||
def breakdown(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> dict[str, float]:
|
|
||||||
return {'pnl': metrics.pnl, 'revenue': metrics.revenue, 'cost': metrics.cost}
|
|
||||||
|
|
||||||
class VolatilityPenalty(BaseObjective):
|
|
||||||
"""Penalize price volatility for user experience."""
|
|
||||||
|
|
||||||
def __init__(self, scale: float = 1.0):
|
|
||||||
self.scale = scale
|
|
||||||
|
|
||||||
def reward(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> float:
|
|
||||||
return -self.scale * metrics.volatility
|
|
||||||
|
|
||||||
def breakdown(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> dict[str, float]:
|
|
||||||
return {'volatility_penalty': -self.scale * metrics.volatility}
|
|
||||||
|
|
||||||
class HoldingCostPenalty(BaseObjective):
|
|
||||||
"""Penalty for inventory holding costs."""
|
|
||||||
|
|
||||||
def __init__(self, scale: float = 1.0):
|
|
||||||
self.scale = scale
|
|
||||||
|
|
||||||
def reward(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> float:
|
|
||||||
return -self.scale * metrics.position_cost
|
|
||||||
|
|
||||||
def breakdown(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> dict[str, float]:
|
|
||||||
return {'holding_cost_penalty': -self.scale * metrics.position_cost}
|
|
||||||
|
|
||||||
class LostOpportunityCostPenalty(BaseObjective):
|
|
||||||
"""Penalty for lost sales due to stockouts or missed fills."""
|
|
||||||
|
|
||||||
def __init__(self, scale: float = 1.0):
|
|
||||||
self.scale = scale
|
|
||||||
|
|
||||||
def reward(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> float:
|
|
||||||
return -self.scale * metrics.lost_opportunity
|
|
||||||
|
|
||||||
def breakdown(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> dict[str, float]:
|
|
||||||
return {'lost_opportunity_penalty': -self.scale * metrics.lost_opportunity}
|
|
||||||
|
|
||||||
class InventoryRiskPenalty(BaseObjective):
|
|
||||||
"""Quadratic inventory risk penalty (Avellaneda-Stoikov style).
|
|
||||||
|
|
||||||
Penalty = gamma * sigma^2 * q^2 / 2, where q is total position.
|
|
||||||
Encourages market makers to keep inventory near zero.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, gamma: float = 0.1, sigma: float = 1.0):
|
|
||||||
self.gamma = gamma
|
|
||||||
self.sigma = sigma
|
|
||||||
|
|
||||||
def reward(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> float:
|
|
||||||
if obs.position is None: return 0.0
|
|
||||||
q = np.sum(obs.position)
|
|
||||||
return -inventory_penalty(q, self.gamma, self.sigma)
|
|
||||||
|
|
||||||
def breakdown(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> dict[str, float]:
|
|
||||||
return {'inventory_risk_penalty': self.reward(quote, instruments, metrics, hidden, obs)}
|
|
||||||
|
|
||||||
class SpreadCaptureReward(BaseObjective):
|
|
||||||
"""Reward for capturing bid-ask spread in market making."""
|
|
||||||
|
|
||||||
def reward(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> float:
|
|
||||||
return metrics.spread_capture
|
|
||||||
|
|
||||||
def breakdown(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState, obs: Observation) -> dict[str, float]:
|
|
||||||
return {'spread_capture': metrics.spread_capture}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
"""
|
|
||||||
Observation construction with demand censoring.
|
|
||||||
|
|
||||||
This module provides the ObservationBuilder that constructs agent observations
|
|
||||||
from step data. The key invariant is that observations only contain censored
|
|
||||||
data (fills) and never true demand, ensuring proper research conditions.
|
|
||||||
|
|
||||||
The ObservationConfig controls what is included in observations:
|
|
||||||
- Position visibility
|
|
||||||
- Market/competitor visibility
|
|
||||||
- Demand proxy method
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import numpy as np
|
|
||||||
from .types import Quote, InstrumentSet, StepLogs, StepMetrics, MarketState, HiddenState, Observation
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ObservationConfig:
|
|
||||||
"""Configuration for observation construction.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
include_position: Include current position in observation
|
|
||||||
include_market: Include market/competitor state in observation
|
|
||||||
mask_true_demand: If True, observation excludes true demand (research mode)
|
|
||||||
demand_proxy: Method for demand proxy ('fills', 'exposures', 'weighted')
|
|
||||||
exposure_weights: Weights for weighted demand proxy
|
|
||||||
"""
|
|
||||||
include_position: bool = True
|
|
||||||
include_market: bool = True
|
|
||||||
mask_true_demand: bool = True
|
|
||||||
demand_proxy: str = 'fills'
|
|
||||||
exposure_weights: dict[str, float] | None = None
|
|
||||||
|
|
||||||
class DefaultObservationBuilder:
|
|
||||||
"""Constructs censored observations for the agent.
|
|
||||||
|
|
||||||
Ensures the key research invariant: observations contain only
|
|
||||||
censored fills (realized sales), never true demand. True demand
|
|
||||||
is placed in the info dict for research analysis only.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cfg: ObservationConfig | None = None):
|
|
||||||
self.cfg = cfg or ObservationConfig()
|
|
||||||
|
|
||||||
def build(self, quote: Quote, instruments: InstrumentSet, logs: StepLogs,
|
|
||||||
metrics: StepMetrics, market: MarketState | None,
|
|
||||||
hidden: HiddenState, mask_demand: bool, t: int) -> Observation:
|
|
||||||
n = instruments.n
|
|
||||||
cfg = self.cfg
|
|
||||||
|
|
||||||
# always show censored fills
|
|
||||||
fills = logs.censored_fills if logs.censored_fills is not None else np.zeros(n)
|
|
||||||
|
|
||||||
# compute exposures from logs
|
|
||||||
if logs.events:
|
|
||||||
exposures = np.zeros(n)
|
|
||||||
for e in logs.events:
|
|
||||||
if e.instrument_id is not None:
|
|
||||||
exposures[e.instrument_id] += 1
|
|
||||||
else:
|
|
||||||
exposures = logs.aggregates.get('exposures', np.zeros(n))
|
|
||||||
|
|
||||||
# position - only if configured and available
|
|
||||||
position = None
|
|
||||||
if cfg.include_position and instruments.position is not None:
|
|
||||||
position = instruments.position.copy()
|
|
||||||
|
|
||||||
# market state - only if configured
|
|
||||||
obs_market = market if cfg.include_market else None
|
|
||||||
|
|
||||||
return Observation(
|
|
||||||
quotes=quote.prices.copy(),
|
|
||||||
position=position,
|
|
||||||
fills=fills,
|
|
||||||
exposures=exposures,
|
|
||||||
market=obs_market,
|
|
||||||
t=t
|
|
||||||
)
|
|
||||||
|
|
||||||
def make_space(self, n_instruments: int, include_market: bool = True) -> dict:
|
|
||||||
"""Returns dict describing observation space for gym"""
|
|
||||||
space = {
|
|
||||||
'quotes': {'shape': (n_instruments,), 'low': 0, 'high': np.inf},
|
|
||||||
'fills': {'shape': (n_instruments,), 'low': 0, 'high': np.inf},
|
|
||||||
'exposures': {'shape': (n_instruments,), 'low': 0, 'high': np.inf},
|
|
||||||
}
|
|
||||||
if self.cfg.include_position:
|
|
||||||
space['position'] = {'shape': (n_instruments,), 'low': -np.inf, 'high': np.inf}
|
|
||||||
if include_market:
|
|
||||||
space['competitor_quotes'] = {'shape': (n_instruments,), 'low': 0, 'high': np.inf}
|
|
||||||
return space
|
|
||||||
@@ -1,285 +0,0 @@
|
|||||||
"""
|
|
||||||
Main simulation platform orchestrating the Quote-Control loop.
|
|
||||||
|
|
||||||
The Platform class is the central coordinator that:
|
|
||||||
1. Receives pricing actions (quotes) from the agent
|
|
||||||
2. Generates arrivals via the ArrivalModel
|
|
||||||
3. Processes executions via Mechanism and ExecutionModel
|
|
||||||
4. Applies position censorship via PositionModel
|
|
||||||
5. Computes metrics and reward via Objective
|
|
||||||
6. Returns censored observations
|
|
||||||
|
|
||||||
Example:
|
|
||||||
>>> from lab.config import make_retail_platform
|
|
||||||
>>> platform = make_retail_platform()
|
|
||||||
>>> result = platform.reset(seed=42)
|
|
||||||
>>> result = platform.step(platform.instruments.refs * 1.1)
|
|
||||||
>>> print(f"PnL: {result.metrics.pnl:.2f}")
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Any
|
|
||||||
import numpy as np
|
|
||||||
from .types import (Quote, Opportunity, Execution, InstrumentSet, StepLogs, StepMetrics,
|
|
||||||
StepEvent, MarketState, HiddenState, Observation, StepResult)
|
|
||||||
from .constants import LogLevel, EventType, Side
|
|
||||||
from .protocols import Mechanism, ArrivalModel, ExecutionModel, PositionModel, MarketModel, ObservationBuilder, Objective
|
|
||||||
from .stock import PositionModel as DefaultPositionModel, PositionConfig
|
|
||||||
from .observation import DefaultObservationBuilder, ObservationConfig
|
|
||||||
from .objectives.factory import retail_objective
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PlatformConfig:
|
|
||||||
"""Configuration for the simulation platform.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
n_instruments: Number of instruments in the simulation
|
|
||||||
max_steps: Maximum steps before episode terminates
|
|
||||||
dt: Time duration per step (affects arrival rates)
|
|
||||||
log_level: Verbosity of logging (NONE, AGG_ONLY, FULL)
|
|
||||||
mask_demand: If True, observations exclude true demand (research mode)
|
|
||||||
seed: Random seed for reproducibility
|
|
||||||
"""
|
|
||||||
n_instruments: int = 10
|
|
||||||
max_steps: int = 1000
|
|
||||||
dt: float = 1.0
|
|
||||||
log_level: LogLevel = LogLevel.AGG_ONLY
|
|
||||||
mask_demand: bool = True
|
|
||||||
seed: int | None = None
|
|
||||||
|
|
||||||
class Platform:
|
|
||||||
"""Main simulation orchestrator implementing Quote -> Arrival -> Execution -> Position.
|
|
||||||
|
|
||||||
The Platform coordinates all components to simulate a pricing environment:
|
|
||||||
- Mechanism: validates quotes and determines execution logic
|
|
||||||
- ArrivalModel: generates demand opportunities
|
|
||||||
- ExecutionModel: computes acceptance probabilities
|
|
||||||
- PositionModel: manages inventory/position and censorship
|
|
||||||
- MarketModel: updates competitor/market state
|
|
||||||
- ObservationBuilder: constructs censored observations
|
|
||||||
- Objective: computes reward from metrics
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
instruments: The instrument set being priced
|
|
||||||
mechanism: Quote validation and execution mechanism
|
|
||||||
arrival: Demand arrival generator
|
|
||||||
execution: Acceptance probability model
|
|
||||||
position: Inventory/position manager
|
|
||||||
market: Competitor/market dynamics (optional)
|
|
||||||
obs_builder: Observation constructor
|
|
||||||
objective: Reward function
|
|
||||||
cfg: Platform configuration
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, instruments: InstrumentSet, mechanism: Mechanism,
|
|
||||||
arrival: ArrivalModel, execution: ExecutionModel,
|
|
||||||
position: PositionModel | None = None,
|
|
||||||
market: MarketModel | None = None,
|
|
||||||
obs_builder: ObservationBuilder | None = None,
|
|
||||||
objective: Objective | None = None,
|
|
||||||
cfg: PlatformConfig | None = None):
|
|
||||||
self.instruments = instruments
|
|
||||||
self.mechanism = mechanism
|
|
||||||
self.arrival = arrival
|
|
||||||
self.execution = execution
|
|
||||||
self.position = position or DefaultPositionModel(PositionConfig())
|
|
||||||
self.market = market
|
|
||||||
self.obs_builder = obs_builder or DefaultObservationBuilder()
|
|
||||||
self.objective = objective or retail_objective()
|
|
||||||
self.cfg = cfg or PlatformConfig(n_instruments=instruments.n)
|
|
||||||
|
|
||||||
self._t: int = 0
|
|
||||||
self._rng: np.random.Generator = np.random.default_rng(self.cfg.seed)
|
|
||||||
self._quote: Quote | None = None
|
|
||||||
self._market_state: MarketState | None = None
|
|
||||||
self._hidden: HiddenState = HiddenState()
|
|
||||||
self._prev_prices: np.ndarray | None = None
|
|
||||||
|
|
||||||
def reset(self, seed: int | None = None) -> StepResult:
|
|
||||||
"""Reset the platform to initial state.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
seed: Random seed (overrides config seed if provided)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Initial StepResult with zeroed metrics and initial observation
|
|
||||||
"""
|
|
||||||
self._t = 0
|
|
||||||
self._rng = np.random.default_rng(seed or self.cfg.seed)
|
|
||||||
self._hidden = HiddenState()
|
|
||||||
self._prev_prices = self.instruments.refs.copy()
|
|
||||||
|
|
||||||
# reset position
|
|
||||||
self.position.reset(self.instruments, self._rng)
|
|
||||||
self.instruments.position = self.position.position
|
|
||||||
|
|
||||||
# initial quote at reference prices
|
|
||||||
self._quote = Quote(prices=self.instruments.refs.copy(), propensity=1.0,
|
|
||||||
metadata={'prev_prices': self._prev_prices})
|
|
||||||
self._quote = self.mechanism.apply_quote(self._quote, self.instruments, self._rng)
|
|
||||||
|
|
||||||
# initial market state
|
|
||||||
if self.market:
|
|
||||||
self._market_state = self.market.step(0, self._quote, self._hidden, self._rng)
|
|
||||||
|
|
||||||
# build initial observation
|
|
||||||
logs = StepLogs(aggregates={'reset': True},
|
|
||||||
true_demand=np.zeros(self.instruments.n),
|
|
||||||
censored_fills=np.zeros(self.instruments.n))
|
|
||||||
metrics = StepMetrics()
|
|
||||||
obs = self.obs_builder.build(self._quote, self.instruments, logs, metrics,
|
|
||||||
self._market_state, self._hidden, self.cfg.mask_demand, 0)
|
|
||||||
|
|
||||||
return StepResult(obs=obs, reward=0.0, terminated=False, truncated=False,
|
|
||||||
info={'true_demand': logs.true_demand}, metrics=metrics,
|
|
||||||
logs=logs, hidden=self._hidden)
|
|
||||||
|
|
||||||
def step(self, action: np.ndarray, propensity: float = 1.0) -> StepResult:
|
|
||||||
"""Execute one simulation step with the given pricing action.
|
|
||||||
|
|
||||||
The step proceeds as follows:
|
|
||||||
1. Apply quote constraints via mechanism
|
|
||||||
2. Update market/competitor state
|
|
||||||
3. Generate arrivals
|
|
||||||
4. Process arrivals -> executions with acceptance check
|
|
||||||
5. Apply position censorship to executions
|
|
||||||
6. Update position state
|
|
||||||
7. Compute metrics (PnL, costs, etc.)
|
|
||||||
8. Build logs with propensities
|
|
||||||
9. Construct censored observation
|
|
||||||
10. Compute reward
|
|
||||||
|
|
||||||
Args:
|
|
||||||
action: Price vector for all instruments
|
|
||||||
propensity: P(action | behavior policy) for OPE logging
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
StepResult containing observation, reward, metrics, logs, and hidden state
|
|
||||||
"""
|
|
||||||
self._t += 1
|
|
||||||
cfg = self.cfg
|
|
||||||
|
|
||||||
# 1. apply quote from action
|
|
||||||
self._quote = Quote(prices=action, propensity=propensity,
|
|
||||||
metadata={'prev_prices': self._prev_prices})
|
|
||||||
self._quote = self.mechanism.apply_quote(self._quote, self.instruments, self._rng)
|
|
||||||
self._prev_prices = self._quote.prices.copy()
|
|
||||||
self._hidden.quote_history.append(self._quote.prices.copy())
|
|
||||||
|
|
||||||
# 2. update market/competitors
|
|
||||||
if self.market:
|
|
||||||
self._market_state = self.market.step(self._t, self._quote, self._hidden, self._rng)
|
|
||||||
self._hidden.market_history.append(self._market_state)
|
|
||||||
|
|
||||||
# 3. generate arrivals
|
|
||||||
opps = self.arrival.sample(self._t, cfg.dt, self.instruments,
|
|
||||||
self._market_state, self._hidden, self._rng)
|
|
||||||
|
|
||||||
# 4. process opportunities -> executions
|
|
||||||
executions: list[Execution] = []
|
|
||||||
events: list[StepEvent] = []
|
|
||||||
true_demand = np.zeros(self.instruments.n)
|
|
||||||
|
|
||||||
for opp in opps:
|
|
||||||
# log exposure
|
|
||||||
if cfg.log_level == LogLevel.FULL:
|
|
||||||
events.append(StepEvent(t=opp.t, type=EventType.EXPOSURE,
|
|
||||||
instrument_id=opp.instrument_id,
|
|
||||||
opportunity_id=opp.id,
|
|
||||||
price=float(self._quote.prices[opp.instrument_id]),
|
|
||||||
propensity=self._quote.propensity))
|
|
||||||
|
|
||||||
# check acceptance
|
|
||||||
prob = self.execution.prob(opp, self._quote, self.instruments,
|
|
||||||
self._market_state, self._rng)
|
|
||||||
if self._rng.random() < prob:
|
|
||||||
# create execution
|
|
||||||
exe = self.mechanism.process_opportunity(opp, self._quote, self.instruments,
|
|
||||||
self._market_state, self._rng)
|
|
||||||
if exe:
|
|
||||||
true_demand[exe.instrument_id] += exe.size_requested
|
|
||||||
# apply position censorship
|
|
||||||
exe = self.position.apply_execution(exe)
|
|
||||||
executions.append(exe)
|
|
||||||
if cfg.log_level == LogLevel.FULL:
|
|
||||||
events.append(StepEvent(t=exe.t, type=EventType.EXECUTION,
|
|
||||||
instrument_id=exe.instrument_id,
|
|
||||||
opportunity_id=exe.opportunity_id,
|
|
||||||
price=exe.price, size=exe.size_filled,
|
|
||||||
propensity=exe.propensity))
|
|
||||||
|
|
||||||
# 5. update position state
|
|
||||||
self.position.step(self._t)
|
|
||||||
self.instruments.position = self.position.position
|
|
||||||
|
|
||||||
# 6. compute metrics
|
|
||||||
censored_fills = np.zeros(self.instruments.n)
|
|
||||||
revenue = 0.0
|
|
||||||
cost = 0.0
|
|
||||||
spread_capture = 0.0
|
|
||||||
|
|
||||||
for exe in executions:
|
|
||||||
censored_fills[exe.instrument_id] += exe.size_filled
|
|
||||||
if exe.side == Side.BUY:
|
|
||||||
revenue += exe.price * exe.size_filled
|
|
||||||
cost += self.instruments.costs[exe.instrument_id] * exe.size_filled
|
|
||||||
else:
|
|
||||||
revenue -= exe.price * exe.size_filled
|
|
||||||
cost -= self.instruments.costs[exe.instrument_id] * exe.size_filled
|
|
||||||
# spread capture for market making
|
|
||||||
if self._quote.spreads is not None and self._market_state and self._market_state.mid_prices is not None:
|
|
||||||
mid = self._market_state.mid_prices[exe.instrument_id]
|
|
||||||
if exe.side == Side.BUY:
|
|
||||||
spread_capture += (exe.price - mid) * exe.size_filled
|
|
||||||
else:
|
|
||||||
spread_capture += (mid - exe.price) * exe.size_filled
|
|
||||||
|
|
||||||
pnl = revenue - cost
|
|
||||||
units = float(np.sum(censored_fills))
|
|
||||||
lost = float(np.sum(true_demand - censored_fills))
|
|
||||||
|
|
||||||
# volatility
|
|
||||||
volatility = 0.0
|
|
||||||
if len(self._hidden.quote_history) > 1:
|
|
||||||
prev = self._hidden.quote_history[-2]
|
|
||||||
volatility = float(np.mean(np.abs(self._quote.prices - prev) / (prev + 1e-8)))
|
|
||||||
|
|
||||||
metrics = StepMetrics(
|
|
||||||
pnl=pnl, revenue=revenue, cost=cost, units_traded=units,
|
|
||||||
position_cost=self.position.holding_cost,
|
|
||||||
lost_opportunity=self.position.shortage_cost + lost * np.mean(self._quote.prices) * 0.1,
|
|
||||||
spread_capture=spread_capture, volatility=volatility,
|
|
||||||
conversion=units / (len(opps) + 1e-8),
|
|
||||||
per_instrument={'fills': censored_fills, 'demand': true_demand}
|
|
||||||
)
|
|
||||||
|
|
||||||
# 7. build logs
|
|
||||||
logs = StepLogs(
|
|
||||||
events=events if cfg.log_level == LogLevel.FULL else None,
|
|
||||||
executions=executions if cfg.log_level == LogLevel.FULL else None,
|
|
||||||
aggregates={'n_arrivals': len(opps), 'n_executions': len(executions),
|
|
||||||
'exposures': np.bincount([o.instrument_id for o in opps],
|
|
||||||
minlength=self.instruments.n).astype(float)},
|
|
||||||
true_demand=true_demand,
|
|
||||||
censored_fills=censored_fills
|
|
||||||
)
|
|
||||||
|
|
||||||
# 8. build observation
|
|
||||||
obs = self.obs_builder.build(self._quote, self.instruments, logs, metrics,
|
|
||||||
self._market_state, self._hidden, cfg.mask_demand, self._t)
|
|
||||||
|
|
||||||
# 9. compute reward
|
|
||||||
reward = self.objective.reward(self._quote, self.instruments, metrics, self._hidden, obs)
|
|
||||||
breakdown = self.objective.breakdown(self._quote, self.instruments, metrics, self._hidden, obs)
|
|
||||||
# print(f"Step {self._t}: Reward={reward:.2f}, Breakdown={breakdown}")
|
|
||||||
|
|
||||||
|
|
||||||
# 10. check termination
|
|
||||||
terminated = self._t >= cfg.max_steps
|
|
||||||
truncated = False
|
|
||||||
|
|
||||||
info = {'true_demand': true_demand, 'breakdown': self.objective.breakdown(
|
|
||||||
self._quote, self.instruments, metrics, self._hidden, obs)}
|
|
||||||
|
|
||||||
return StepResult(obs=obs, reward=reward, terminated=terminated, truncated=truncated,
|
|
||||||
info=info, metrics=metrics, logs=logs, hidden=self._hidden)
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
"""
|
|
||||||
Protocol definitions for pluggable simulator components.
|
|
||||||
|
|
||||||
This module defines the interfaces (Protocols) that allow swapping different
|
|
||||||
implementations for each stage of the Quote -> Arrival -> Execution -> Position
|
|
||||||
pipeline. All protocols use structural subtyping (duck typing).
|
|
||||||
|
|
||||||
Protocols:
|
|
||||||
Mechanism: How quotes translate to executions (posted price, two-sided, auction)
|
|
||||||
ArrivalModel: How opportunities arrive (Poisson, Hawkes, sessions)
|
|
||||||
ExecutionModel: Acceptance probability given quote (elasticity, intensity)
|
|
||||||
PositionModel: Inventory/position management and censorship
|
|
||||||
MarketModel: Competitor/market dynamics
|
|
||||||
ObservationBuilder: Constructs agent observations with censoring
|
|
||||||
Objective: Computes reward from metrics
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from typing import Protocol, Any, TYPE_CHECKING
|
|
||||||
import numpy as np
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from .types import (Quote, Opportunity, Execution, InstrumentSet, StepLogs,
|
|
||||||
StepMetrics, HiddenState, Observation, MarketState)
|
|
||||||
from .constants import LogLevel
|
|
||||||
|
|
||||||
class Mechanism(Protocol):
|
|
||||||
"""Defines how quotes translate to executions.
|
|
||||||
|
|
||||||
The Mechanism is the core abstraction that differentiates pricing domains:
|
|
||||||
- PostedPrice: single price, buyer decides to purchase or not
|
|
||||||
- TwoSided: bid/ask spread, execution depends on distance from mid
|
|
||||||
- Auction: reserve price affects win probability and clearing price
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
apply_quote: Enforce constraints and return valid quote
|
|
||||||
process_opportunity: Determine execution given opportunity and quote
|
|
||||||
"""
|
|
||||||
def apply_quote(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
rng: np.random.Generator) -> Quote:
|
|
||||||
"""Apply mechanism-specific constraints to a quote.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
quote: Raw quote from policy
|
|
||||||
instruments: Current instrument set with costs/refs
|
|
||||||
rng: Random generator for stochastic constraints
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Constrained quote satisfying mechanism rules (min margin, max delta, etc.)
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
def process_opportunity(self, opp: Opportunity, quote: Quote,
|
|
||||||
instruments: InstrumentSet, market: MarketState | None,
|
|
||||||
rng: np.random.Generator) -> Execution | None:
|
|
||||||
"""Process an opportunity against the current quote.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
opp: Incoming opportunity (session, order, request)
|
|
||||||
quote: Current posted quote
|
|
||||||
instruments: Instrument set
|
|
||||||
market: Current market state (competitor prices, mid-prices)
|
|
||||||
rng: Random generator
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Execution if opportunity converts, None otherwise
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
class ArrivalModel(Protocol):
|
|
||||||
"""Generates opportunities (demand arrivals) for each step.
|
|
||||||
|
|
||||||
Different arrival models capture different demand dynamics:
|
|
||||||
- Poisson: constant rate, memoryless
|
|
||||||
- Hawkes: self-exciting, clustered arrivals
|
|
||||||
- Session: retail browsing with multi-product views
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
sample: Generate opportunities for a time interval
|
|
||||||
"""
|
|
||||||
def sample(self, t: float, dt: float, instruments: InstrumentSet,
|
|
||||||
market: MarketState | None, hidden: HiddenState,
|
|
||||||
rng: np.random.Generator) -> list[Opportunity]:
|
|
||||||
"""Sample opportunities for time interval [t, t+dt).
|
|
||||||
|
|
||||||
Args:
|
|
||||||
t: Current time
|
|
||||||
dt: Time interval length
|
|
||||||
instruments: Available instruments
|
|
||||||
market: Current market state
|
|
||||||
hidden: Hidden state (contains demand intensity, contamination)
|
|
||||||
rng: Random generator
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
List of opportunities arriving in this interval
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
class ExecutionModel(Protocol):
|
|
||||||
"""Computes acceptance/execution probability given quote and context.
|
|
||||||
|
|
||||||
Different models capture different demand responses:
|
|
||||||
- Elasticity: price sensitivity with competitor cross-effects
|
|
||||||
- Intensity: distance-based fill probability (market making)
|
|
||||||
- Logit: discrete choice model
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
prob: Compute acceptance probability
|
|
||||||
uncensor: Estimate true demand from censored fills
|
|
||||||
"""
|
|
||||||
def prob(self, opp: Opportunity, quote: Quote, instruments: InstrumentSet,
|
|
||||||
market: MarketState | None, rng: np.random.Generator) -> float:
|
|
||||||
"""Compute probability that opportunity accepts the quote.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
opp: Opportunity to evaluate
|
|
||||||
quote: Current quote
|
|
||||||
instruments: Instrument set
|
|
||||||
market: Market state (competitor prices affect cross-elasticity)
|
|
||||||
rng: Random generator
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Probability in [0, 1] that opportunity executes
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
def uncensor(self, fills: np.ndarray, instruments: InstrumentSet,
|
|
||||||
context: dict[str, Any] | None = None) -> np.ndarray:
|
|
||||||
"""Estimate true demand from censored fills.
|
|
||||||
|
|
||||||
Used for demand estimation research under inventory censorship.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
fills: Observed (censored) fill counts
|
|
||||||
instruments: Instrument set
|
|
||||||
context: Additional context (exposures, prices shown)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Estimated true demand counts
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
class PositionModel(Protocol):
|
|
||||||
"""Manages inventory (retail) or position (finance).
|
|
||||||
|
|
||||||
Handles:
|
|
||||||
- Position constraints and censorship
|
|
||||||
- Holding costs (retail) or inventory risk (finance)
|
|
||||||
- Replenishment and order receipt
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
reset: Initialize position state
|
|
||||||
available: Query available capacity for a trade
|
|
||||||
apply_execution: Censor execution by available position
|
|
||||||
step: Process time-based updates (replenishment, holding cost)
|
|
||||||
|
|
||||||
Properties:
|
|
||||||
position: Current position vector
|
|
||||||
holding_cost: Cost incurred this step from holding position
|
|
||||||
"""
|
|
||||||
def reset(self, instruments: InstrumentSet, rng: np.random.Generator) -> None:
|
|
||||||
"""Initialize position state for new episode."""
|
|
||||||
...
|
|
||||||
|
|
||||||
def available(self, instrument_id: int, side: Any) -> float:
|
|
||||||
"""Query available capacity for a trade.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
instrument_id: Which instrument
|
|
||||||
side: BUY or SELL
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Maximum tradeable size given current position
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
def apply_execution(self, exe: Execution) -> Execution:
|
|
||||||
"""Apply position constraints to an execution.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
exe: Proposed execution with size_requested
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Censored execution with size_filled <= available capacity
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
def step(self, t: float) -> None:
|
|
||||||
"""Process time-based position updates.
|
|
||||||
|
|
||||||
Handles replenishment receipt, holding cost calculation, etc.
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
@property
|
|
||||||
def position(self) -> np.ndarray:
|
|
||||||
"""Current position vector (positive=long/inventory, negative=short)."""
|
|
||||||
...
|
|
||||||
|
|
||||||
@property
|
|
||||||
def holding_cost(self) -> float:
|
|
||||||
"""Holding cost incurred this step."""
|
|
||||||
...
|
|
||||||
|
|
||||||
class MarketModel(Protocol):
|
|
||||||
"""Models external market dynamics and competitor behavior.
|
|
||||||
|
|
||||||
For retail: competitor price dynamics (static, reactive, stochastic)
|
|
||||||
For finance: mid-price process (GBM, mean-reverting)
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
step: Update market state given agent's quotes
|
|
||||||
"""
|
|
||||||
def step(self, t: float, self_quotes: Quote, hidden: HiddenState,
|
|
||||||
rng: np.random.Generator) -> MarketState:
|
|
||||||
"""Update market state for this timestep.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
t: Current time
|
|
||||||
self_quotes: Agent's current quotes (competitors may react)
|
|
||||||
hidden: Hidden state (regime info)
|
|
||||||
rng: Random generator
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Updated market state with competitor prices, mid-prices, volatility
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
class ObservationBuilder(Protocol):
|
|
||||||
"""Constructs agent observations with appropriate censoring.
|
|
||||||
|
|
||||||
Critical for research: ensures agent only sees censored fills,
|
|
||||||
never true demand (which goes in info dict).
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
build: Construct observation from step data
|
|
||||||
"""
|
|
||||||
def build(self, quote: Quote, instruments: InstrumentSet, logs: StepLogs,
|
|
||||||
metrics: StepMetrics, market: MarketState | None,
|
|
||||||
hidden: HiddenState, mask_demand: bool, t: int) -> Observation:
|
|
||||||
"""Build observation for agent.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
quote: Current quote
|
|
||||||
instruments: Instrument set with positions
|
|
||||||
logs: Step logs with true_demand and censored_fills
|
|
||||||
metrics: Computed metrics
|
|
||||||
market: Market state
|
|
||||||
hidden: Hidden state (not included in obs)
|
|
||||||
mask_demand: If True, exclude true demand from observation
|
|
||||||
t: Current timestep
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Observation containing only observable quantities
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
class Objective(Protocol):
|
|
||||||
"""Computes reward from step metrics.
|
|
||||||
|
|
||||||
Supports composite objectives with weighted terms:
|
|
||||||
- PnL (profit)
|
|
||||||
- Position costs (holding, inventory risk)
|
|
||||||
- Lost opportunity (stockouts)
|
|
||||||
- Volatility penalty (UX)
|
|
||||||
- Spread capture (market making)
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
reward: Compute scalar reward
|
|
||||||
breakdown: Get per-term contribution for analysis
|
|
||||||
"""
|
|
||||||
def reward(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState,
|
|
||||||
obs: Observation) -> float:
|
|
||||||
"""Compute scalar reward for this step.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
quote: Current quote
|
|
||||||
instruments: Instrument set
|
|
||||||
metrics: Step metrics (pnl, costs, etc.)
|
|
||||||
hidden: Hidden state
|
|
||||||
obs: Agent observation
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Scalar reward value
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
|
|
||||||
def breakdown(self, quote: Quote, instruments: InstrumentSet,
|
|
||||||
metrics: StepMetrics, hidden: HiddenState,
|
|
||||||
obs: Observation) -> dict[str, float]:
|
|
||||||
"""Get reward breakdown by component.
|
|
||||||
|
|
||||||
Useful for analyzing which terms dominate the reward.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict mapping term names to their contributions
|
|
||||||
"""
|
|
||||||
...
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
"""
|
|
||||||
Inventory/position management and instrument factories.
|
|
||||||
|
|
||||||
This module provides:
|
|
||||||
- PositionConfig: Configuration for position constraints and costs
|
|
||||||
- PositionModel: Manages inventory (retail) or position (finance)
|
|
||||||
- make_instruments: Factory for creating instrument sets
|
|
||||||
|
|
||||||
The PositionModel handles demand censorship by limiting executions
|
|
||||||
to available inventory, computing holding costs, and managing replenishment.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
import numpy as np
|
|
||||||
from .types import Instrument, InstrumentSet, Execution
|
|
||||||
from .constants import Side, InstrumentType
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PositionConfig:
|
|
||||||
"""Configuration for position/inventory management.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
initial_position: Starting inventory (None = unlimited, float = same for all)
|
|
||||||
max_position: Maximum long position per instrument
|
|
||||||
min_position: Maximum short position (negative, for finance)
|
|
||||||
holding_cost_rate: Cost per unit per step for holding inventory
|
|
||||||
shortage_cost_rate: Opportunity cost rate for stockouts
|
|
||||||
lead_time: Steps until replenishment orders arrive
|
|
||||||
"""
|
|
||||||
initial_position: np.ndarray | float | None = None
|
|
||||||
max_position: float = 1000.0
|
|
||||||
min_position: float = -1000.0
|
|
||||||
holding_cost_rate: float = 0.001
|
|
||||||
shortage_cost_rate: float = 0.05
|
|
||||||
lead_time: int = 0
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PositionModel:
|
|
||||||
"""Manages inventory (retail) or position (finance) with censorship.
|
|
||||||
|
|
||||||
Key responsibilities:
|
|
||||||
- Track current position per instrument
|
|
||||||
- Censor executions when position is insufficient
|
|
||||||
- Compute holding costs per step
|
|
||||||
- Track shortage/stockout costs
|
|
||||||
- Handle replenishment orders with lead time
|
|
||||||
|
|
||||||
For retail: position is inventory (positive), selling reduces it
|
|
||||||
For finance: position can be positive (long) or negative (short)
|
|
||||||
"""
|
|
||||||
cfg: PositionConfig
|
|
||||||
n: int = 0
|
|
||||||
_position: np.ndarray = field(default_factory=lambda: np.array([]))
|
|
||||||
_pending_orders: list[tuple[int, np.ndarray]] = field(default_factory=list)
|
|
||||||
_step_holding_cost: float = 0.0
|
|
||||||
_step_shortage_cost: float = 0.0
|
|
||||||
|
|
||||||
def reset(self, instruments: InstrumentSet, rng: np.random.Generator) -> None:
|
|
||||||
self.n = instruments.n
|
|
||||||
if self.cfg.initial_position is None:
|
|
||||||
self._position = np.full(self.n, np.inf) # unlimited
|
|
||||||
elif isinstance(self.cfg.initial_position, (int, float)):
|
|
||||||
self._position = np.full(self.n, float(self.cfg.initial_position))
|
|
||||||
else:
|
|
||||||
self._position = self.cfg.initial_position.copy().astype(np.float64)
|
|
||||||
self._pending_orders = []
|
|
||||||
self._step_holding_cost = 0.0
|
|
||||||
self._step_shortage_cost = 0.0
|
|
||||||
|
|
||||||
def available(self, instrument_id: int, side: Side) -> float:
|
|
||||||
pos = self._position[instrument_id]
|
|
||||||
if np.isinf(pos): return np.inf
|
|
||||||
if side == Side.BUY:
|
|
||||||
return max(0, pos) # can sell up to current inventory
|
|
||||||
else:
|
|
||||||
return max(0, self.cfg.max_position - pos) # can buy up to max
|
|
||||||
|
|
||||||
def apply_execution(self, exe: Execution) -> Execution:
|
|
||||||
idx = int(exe.instrument_id)
|
|
||||||
avail = self.available(idx, exe.side)
|
|
||||||
filled = min(exe.size_requested, avail)
|
|
||||||
shortage = exe.size_requested - filled
|
|
||||||
|
|
||||||
if exe.side == Side.BUY:
|
|
||||||
self._position[idx] -= filled # sold from inventory
|
|
||||||
else:
|
|
||||||
self._position[idx] += filled # bought into inventory
|
|
||||||
|
|
||||||
if shortage > 0:
|
|
||||||
self._step_shortage_cost += shortage * exe.price * self.cfg.shortage_cost_rate
|
|
||||||
|
|
||||||
return Execution(
|
|
||||||
opportunity_id=exe.opportunity_id, instrument_id=exe.instrument_id,
|
|
||||||
side=exe.side, size_requested=exe.size_requested,
|
|
||||||
size_filled=filled, price=exe.price, propensity=exe.propensity, t=exe.t
|
|
||||||
)
|
|
||||||
|
|
||||||
def order(self, quantity: np.ndarray) -> None:
|
|
||||||
if self.cfg.lead_time > 0:
|
|
||||||
self._pending_orders.append((self.cfg.lead_time, quantity.copy()))
|
|
||||||
else:
|
|
||||||
self._position += quantity
|
|
||||||
|
|
||||||
def step(self, t: float) -> None:
|
|
||||||
# compute holding cost
|
|
||||||
pos = np.where(np.isinf(self._position), 0, self._position)
|
|
||||||
self._step_holding_cost = float(np.sum(np.abs(pos)) * self.cfg.holding_cost_rate)
|
|
||||||
|
|
||||||
# receive pending orders
|
|
||||||
new_pending = []
|
|
||||||
for (remaining, qty) in self._pending_orders:
|
|
||||||
if remaining <= 1:
|
|
||||||
self._position += qty
|
|
||||||
else:
|
|
||||||
new_pending.append((remaining - 1, qty))
|
|
||||||
self._pending_orders = new_pending
|
|
||||||
|
|
||||||
@property
|
|
||||||
def position(self) -> np.ndarray:
|
|
||||||
return np.where(np.isinf(self._position), -1, self._position)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def holding_cost(self) -> float:
|
|
||||||
return self._step_holding_cost
|
|
||||||
|
|
||||||
@property
|
|
||||||
def shortage_cost(self) -> float:
|
|
||||||
return self._step_shortage_cost
|
|
||||||
|
|
||||||
def make_instruments(n: int, cost_range: tuple[float, float] = (1.0, 10.0),
|
|
||||||
margin_range: tuple[float, float] = (0.2, 0.5),
|
|
||||||
inst_type: InstrumentType = InstrumentType.SKU,
|
|
||||||
rng: np.random.Generator | None = None) -> InstrumentSet:
|
|
||||||
"""Factory function to create a random instrument set.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
n: Number of instruments to create
|
|
||||||
cost_range: (min, max) for uniform cost sampling
|
|
||||||
margin_range: (min, max) for uniform margin sampling
|
|
||||||
inst_type: Type of instruments (SKU, ASSET, etc.)
|
|
||||||
rng: Random generator (uses default if None)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
InstrumentSet with n instruments having random costs and margins
|
|
||||||
"""
|
|
||||||
rng = rng or np.random.default_rng()
|
|
||||||
costs = rng.uniform(*cost_range, n)
|
|
||||||
margins = rng.uniform(*margin_range, n)
|
|
||||||
items = [Instrument(id=i, type=inst_type, cost_basis=c, reference_price=c*(1+m))
|
|
||||||
for i, (c, m) in enumerate(zip(costs, margins))]
|
|
||||||
return InstrumentSet(instruments=items)
|
|
||||||
@@ -1,318 +0,0 @@
|
|||||||
"""
|
|
||||||
Core data types for the Quote-Control simulator.
|
|
||||||
|
|
||||||
This module defines the fundamental data structures used throughout the platform:
|
|
||||||
- Identifiers (InstrumentId, OpportunityId, AgentId)
|
|
||||||
- Domain objects (Instrument, Quote, Opportunity, Execution)
|
|
||||||
- Logging structures (StepEvent, StepLogs, StepMetrics)
|
|
||||||
- State containers (MarketState, HiddenState, Observation, StepResult)
|
|
||||||
|
|
||||||
All dataclasses are designed to be serializable and numpy-compatible.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass, field
|
|
||||||
from typing import Any, NewType
|
|
||||||
import numpy as np
|
|
||||||
from .constants import Side, InstrumentType, OpportunityType, EventType
|
|
||||||
|
|
||||||
InstrumentId = NewType('InstrumentId', int) # unique instrument index
|
|
||||||
OpportunityId = NewType('OpportunityId', str) # unique opportunity/session ID
|
|
||||||
AgentId = NewType('AgentId', str) # unique agent/actor ID
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Instrument:
|
|
||||||
"""Represents a priceable entity in the simulation.
|
|
||||||
|
|
||||||
An instrument can be a retail SKU, financial asset, loan product, or subscription.
|
|
||||||
The cost_basis represents the fundamental value (marginal cost for retail,
|
|
||||||
mid-price for assets, funding rate for loans).
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
id: Unique identifier for this instrument
|
|
||||||
type: Category of instrument (SKU, ASSET, LOAN, SUBSCRIPTION)
|
|
||||||
cost_basis: Fundamental cost or value (marginal cost, mid-price, funding rate)
|
|
||||||
reference_price: Base or fair price used for action scaling
|
|
||||||
attrs: Additional attributes (quality score, category, volatility, etc.)
|
|
||||||
"""
|
|
||||||
id: InstrumentId
|
|
||||||
type: InstrumentType
|
|
||||||
cost_basis: float
|
|
||||||
reference_price: float
|
|
||||||
attrs: dict[str, Any] = field(default_factory=dict)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class InstrumentSet:
|
|
||||||
"""Collection of instruments with optional position tracking.
|
|
||||||
|
|
||||||
Provides vectorized access to instrument properties for efficient computation.
|
|
||||||
Position can be positive (long/inventory) or negative (short) for financial assets.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
instruments: List of Instrument objects
|
|
||||||
position: Current position per instrument (None = unlimited capacity)
|
|
||||||
|
|
||||||
Properties:
|
|
||||||
n: Number of instruments
|
|
||||||
costs: Vector of cost bases
|
|
||||||
refs: Vector of reference prices
|
|
||||||
"""
|
|
||||||
instruments: list[Instrument]
|
|
||||||
position: np.ndarray | None = None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def n(self) -> int: return len(self.instruments)
|
|
||||||
@property
|
|
||||||
def costs(self) -> np.ndarray: return np.array([i.cost_basis for i in self.instruments], np.float32)
|
|
||||||
@property
|
|
||||||
def refs(self) -> np.ndarray: return np.array([i.reference_price for i in self.instruments], np.float32)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Quote:
|
|
||||||
"""Price quote set by the policy - the action in the MDP.
|
|
||||||
|
|
||||||
Supports multiple quoting mechanisms:
|
|
||||||
- Posted price: only `prices` field used
|
|
||||||
- Two-sided: `prices` as mid, `spreads` for bid-ask width
|
|
||||||
- Auction: `prices` as reserve prices
|
|
||||||
|
|
||||||
The propensity field is critical for off-policy evaluation (OPE).
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
prices: Posted prices (retail) or mid-quotes (market making)
|
|
||||||
spreads: Bid-ask spread width for two-sided quoting (None for posted price)
|
|
||||||
propensity: P(this quote | behavior policy) for importance sampling
|
|
||||||
metadata: Additional info (prev_prices for delta constraints, etc.)
|
|
||||||
|
|
||||||
Properties:
|
|
||||||
bids: Computed bid prices (mid - spread/2)
|
|
||||||
asks: Computed ask prices (mid + spread/2)
|
|
||||||
"""
|
|
||||||
prices: np.ndarray
|
|
||||||
spreads: np.ndarray | None = None
|
|
||||||
propensity: float = 1.0
|
|
||||||
metadata: dict[str, Any] = field(default_factory=dict)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def bids(self) -> np.ndarray | None:
|
|
||||||
return self.prices - self.spreads/2 if self.spreads is not None else None
|
|
||||||
@property
|
|
||||||
def asks(self) -> np.ndarray | None:
|
|
||||||
return self.prices + self.spreads/2 if self.spreads is not None else None
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Opportunity:
|
|
||||||
"""An arrival event that may result in a transaction.
|
|
||||||
|
|
||||||
Opportunities are the demand side of the simulation:
|
|
||||||
- Retail: browsing session with purchase intent
|
|
||||||
- Market making: incoming market order
|
|
||||||
- Lending: loan application
|
|
||||||
|
|
||||||
The context dict carries segment/type information used by execution models.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
id: Unique identifier for this opportunity
|
|
||||||
type: Category (SESSION, MARKET_ORDER, REQUEST)
|
|
||||||
side: BUY or SELL intent
|
|
||||||
instrument_id: Which instrument the opportunity targets
|
|
||||||
size: Requested transaction size (units, shares, principal)
|
|
||||||
t: Arrival timestamp
|
|
||||||
context: Segment info (is_scraper, credit_score, urgency, etc.)
|
|
||||||
"""
|
|
||||||
id: OpportunityId
|
|
||||||
type: OpportunityType
|
|
||||||
side: Side
|
|
||||||
instrument_id: InstrumentId
|
|
||||||
size: float = 1.0
|
|
||||||
t: float = 0.0
|
|
||||||
context: dict[str, Any] = field(default_factory=dict)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Execution:
|
|
||||||
"""A realized transaction after acceptance and position censorship.
|
|
||||||
|
|
||||||
The difference between size_requested and size_filled represents
|
|
||||||
censored demand due to inventory/position constraints.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
opportunity_id: Links back to the originating Opportunity
|
|
||||||
instrument_id: Which instrument was traded
|
|
||||||
side: BUY or SELL
|
|
||||||
size_requested: Original requested size (true demand)
|
|
||||||
size_filled: Actual filled size after censorship
|
|
||||||
price: Execution price
|
|
||||||
propensity: Combined propensity for OPE (quote * acceptance)
|
|
||||||
t: Execution timestamp
|
|
||||||
"""
|
|
||||||
opportunity_id: OpportunityId
|
|
||||||
instrument_id: InstrumentId
|
|
||||||
side: Side
|
|
||||||
size_requested: float
|
|
||||||
size_filled: float
|
|
||||||
price: float
|
|
||||||
propensity: float = 1.0
|
|
||||||
t: float = 0.0
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class StepEvent:
|
|
||||||
"""Generic logged event"""
|
|
||||||
t: float
|
|
||||||
type: EventType
|
|
||||||
instrument_id: InstrumentId | None = None
|
|
||||||
opportunity_id: OpportunityId | None = None
|
|
||||||
price: float | None = None
|
|
||||||
size: float | None = None
|
|
||||||
propensity: float = 1.0
|
|
||||||
metadata: dict[str, Any] = field(default_factory=dict)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class StepLogs:
|
|
||||||
"""Container for all logging data from a simulation step.
|
|
||||||
|
|
||||||
Supports both detailed event logging (for OPE) and aggregate-only mode
|
|
||||||
(for fast simulation). The true_demand vs censored_fills distinction
|
|
||||||
is critical for research on demand estimation under censorship.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
events: Detailed event log (None if LogLevel != FULL)
|
|
||||||
executions: List of executed transactions (None if LogLevel != FULL)
|
|
||||||
aggregates: Always-available aggregate statistics
|
|
||||||
true_demand: Oracle demand before censorship (for research, not in obs)
|
|
||||||
censored_fills: Realized fills after position constraints (observable)
|
|
||||||
"""
|
|
||||||
events: list[StepEvent] | None = None
|
|
||||||
executions: list[Execution] | None = None
|
|
||||||
aggregates: dict[str, Any] = field(default_factory=dict)
|
|
||||||
true_demand: np.ndarray | None = None
|
|
||||||
censored_fills: np.ndarray | None = None
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class StepMetrics:
|
|
||||||
"""Computed metrics for a single simulation step.
|
|
||||||
|
|
||||||
Metrics are domain-aware: retail uses revenue/cost/holding_cost,
|
|
||||||
market making uses spread_capture and inventory risk.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
pnl: Profit and loss (revenue - cost for retail, mark-to-market for finance)
|
|
||||||
revenue: Gross revenue from sales/executions
|
|
||||||
cost: Cost of goods sold or position acquisition cost
|
|
||||||
units_traded: Total units/shares transacted
|
|
||||||
position_cost: Holding cost (retail) or inventory risk penalty (finance)
|
|
||||||
lost_opportunity: Cost of stockouts or missed fills
|
|
||||||
spread_capture: Bid-ask spread captured (market making)
|
|
||||||
volatility: Price volatility metric for UX consideration
|
|
||||||
conversion: Fill rate (executions / opportunities)
|
|
||||||
per_instrument: Per-instrument breakdowns (fills, demand, etc.)
|
|
||||||
"""
|
|
||||||
pnl: float = 0.0
|
|
||||||
revenue: float = 0.0
|
|
||||||
cost: float = 0.0
|
|
||||||
units_traded: float = 0.0
|
|
||||||
position_cost: float = 0.0
|
|
||||||
lost_opportunity: float = 0.0
|
|
||||||
spread_capture: float = 0.0
|
|
||||||
volatility: float = 0.0
|
|
||||||
conversion: float = 0.0
|
|
||||||
per_instrument: dict[str, np.ndarray] = field(default_factory=dict)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class MarketState:
|
|
||||||
"""External market conditions and competitor state.
|
|
||||||
|
|
||||||
For retail: competitor_quotes drives cross-elasticity effects.
|
|
||||||
For finance: mid_prices and volatility drive execution dynamics.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
competitor_quotes: Competitor posted prices (retail)
|
|
||||||
mid_prices: Market mid-prices for assets (finance)
|
|
||||||
volatility: Per-instrument volatility estimate
|
|
||||||
regime: Market regime identifier (normal, price_war, high_vol, etc.)
|
|
||||||
t: Timestamp of this market state
|
|
||||||
"""
|
|
||||||
competitor_quotes: np.ndarray | None = None
|
|
||||||
mid_prices: np.ndarray | None = None
|
|
||||||
volatility: np.ndarray | None = None
|
|
||||||
regime: str = 'normal'
|
|
||||||
t: float = 0.0
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class HiddenState:
|
|
||||||
"""Internal simulator state not exposed to the agent.
|
|
||||||
|
|
||||||
Contains oracle information for research analysis and
|
|
||||||
history needed for non-stationary dynamics.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
true_demand_intensity: Latent demand multiplier
|
|
||||||
contamination: Fraction of arrivals that are adversarial/scraper
|
|
||||||
regime: Current market/competitor regime
|
|
||||||
quote_history: History of agent quotes for volatility calculation
|
|
||||||
market_history: History of market states for analysis
|
|
||||||
"""
|
|
||||||
true_demand_intensity: float = 1.0
|
|
||||||
contamination: float = 0.0
|
|
||||||
regime: str = 'normal'
|
|
||||||
quote_history: list[np.ndarray] = field(default_factory=list)
|
|
||||||
market_history: list[MarketState] = field(default_factory=list)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Observation:
|
|
||||||
"""Observable state provided to the agent - censored view only.
|
|
||||||
|
|
||||||
Critical invariant: Observation never contains true_demand, only
|
|
||||||
censored fills. This enforces the censorship research setting.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
quotes: Current posted quotes (the agent's last action)
|
|
||||||
position: Current inventory/position state
|
|
||||||
fills: Censored execution counts per instrument
|
|
||||||
exposures: Opportunity exposure counts per instrument
|
|
||||||
market: Observable market state (competitor prices, volatility)
|
|
||||||
t: Current timestep
|
|
||||||
extra: Additional observable features
|
|
||||||
|
|
||||||
Methods:
|
|
||||||
to_flat: Flatten to numpy array for gym compatibility
|
|
||||||
"""
|
|
||||||
quotes: np.ndarray
|
|
||||||
position: np.ndarray | None
|
|
||||||
fills: np.ndarray
|
|
||||||
exposures: np.ndarray
|
|
||||||
market: MarketState | None
|
|
||||||
t: int
|
|
||||||
extra: dict[str, Any] = field(default_factory=dict)
|
|
||||||
|
|
||||||
def to_flat(self) -> np.ndarray:
|
|
||||||
"""Flatten observation to 1D numpy array for gym environments."""
|
|
||||||
parts = [self.quotes, self.fills, self.exposures]
|
|
||||||
if self.position is not None: parts.append(self.position)
|
|
||||||
if self.market and self.market.competitor_quotes is not None:
|
|
||||||
parts.append(self.market.competitor_quotes)
|
|
||||||
return np.concatenate([p.flatten() for p in parts])
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class StepResult:
|
|
||||||
"""Complete result from a simulation step.
|
|
||||||
|
|
||||||
Follows gymnasium convention for obs, reward, terminated, truncated, info.
|
|
||||||
Additionally provides metrics, logs, and hidden state for research.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
obs: Observable state (censored)
|
|
||||||
reward: Scalar reward from objective function
|
|
||||||
terminated: Episode ended naturally (max_steps reached)
|
|
||||||
truncated: Episode ended early (bankruptcy, constraint violation)
|
|
||||||
info: Additional info dict (contains true_demand for research)
|
|
||||||
metrics: Computed metrics for this step
|
|
||||||
logs: Event logs and aggregates
|
|
||||||
hidden: Internal simulator state (oracle info)
|
|
||||||
"""
|
|
||||||
obs: Observation
|
|
||||||
reward: float
|
|
||||||
terminated: bool
|
|
||||||
truncated: bool
|
|
||||||
info: dict[str, Any]
|
|
||||||
metrics: StepMetrics
|
|
||||||
logs: StepLogs
|
|
||||||
hidden: HiddenState
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
from .arrivals import PoissonArrivalModel, HawkesArrivalModel, SessionArrivalModel
|
|
||||||
from .execution import ElasticityExecutionModel, IntensityExecutionModel, LogitExecutionModel
|
|
||||||
from .competitors import (StaticCompetitorModel, ReactiveCompetitorModel,
|
|
||||||
StochasticCompetitorModel, GBMMarketModel)
|
|
||||||
|
|
||||||
__all__ = [
|
|
||||||
'PoissonArrivalModel', 'HawkesArrivalModel', 'SessionArrivalModel',
|
|
||||||
'ElasticityExecutionModel', 'IntensityExecutionModel', 'LogitExecutionModel',
|
|
||||||
'StaticCompetitorModel', 'ReactiveCompetitorModel', 'StochasticCompetitorModel', 'GBMMarketModel',
|
|
||||||
]
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
"""
|
|
||||||
Arrival models for generating demand opportunities.
|
|
||||||
|
|
||||||
This module provides different arrival processes:
|
|
||||||
- PoissonArrivalModel: Constant-rate memoryless arrivals
|
|
||||||
- HawkesArrivalModel: Self-exciting clustered arrivals (market orders)
|
|
||||||
- SessionArrivalModel: Retail browsing sessions with multi-product views
|
|
||||||
|
|
||||||
Each model implements the ArrivalModel protocol and generates Opportunity objects
|
|
||||||
that flow through the execution pipeline.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Callable
|
|
||||||
import numpy as np
|
|
||||||
from uuid import uuid4
|
|
||||||
from ..outlet.types import Opportunity, InstrumentSet, MarketState, HiddenState
|
|
||||||
from ..outlet.constants import Side, OpportunityType
|
|
||||||
from ..outlet.math_util import poisson_arrivals, hawkes_intensity
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class PoissonArrivalConfig:
|
|
||||||
"""Configuration for Poisson arrival process.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
base_rate: Expected arrivals per unit time (scaled by hidden.true_demand_intensity)
|
|
||||||
side_probs: Probability distribution over BUY/SELL sides
|
|
||||||
"""
|
|
||||||
base_rate: float = 10.0
|
|
||||||
side_probs: dict[Side, float] = None
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
if self.side_probs is None:
|
|
||||||
self.side_probs = {Side.BUY: 1.0}
|
|
||||||
|
|
||||||
class PoissonArrivalModel:
|
|
||||||
"""Homogeneous Poisson arrival process.
|
|
||||||
|
|
||||||
Generates arrivals at a constant rate (modulated by demand intensity).
|
|
||||||
Suitable for stationary demand or as a baseline model.
|
|
||||||
|
|
||||||
The actual arrival count follows Poisson(rate * dt * intensity).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cfg: PoissonArrivalConfig | None = None):
|
|
||||||
self.cfg = cfg or PoissonArrivalConfig()
|
|
||||||
|
|
||||||
def sample(self, t: float, dt: float, instruments: InstrumentSet,
|
|
||||||
market: MarketState | None, hidden: HiddenState,
|
|
||||||
rng: np.random.Generator) -> list[Opportunity]:
|
|
||||||
n_arrivals = poisson_arrivals(self.cfg.base_rate * hidden.true_demand_intensity, dt, rng)
|
|
||||||
opps = []
|
|
||||||
for _ in range(n_arrivals):
|
|
||||||
inst_id = rng.integers(0, instruments.n)
|
|
||||||
side = rng.choice(list(self.cfg.side_probs.keys()),
|
|
||||||
p=list(self.cfg.side_probs.values()))
|
|
||||||
opps.append(Opportunity(
|
|
||||||
id=str(uuid4())[:8], type=OpportunityType.SESSION,
|
|
||||||
side=side, instrument_id=inst_id, size=1.0, t=t,
|
|
||||||
context={'segment': 'default'}
|
|
||||||
))
|
|
||||||
return opps
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class HawkesArrivalConfig:
|
|
||||||
"""Configuration for Hawkes self-exciting process.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
base_rate: Baseline arrival intensity
|
|
||||||
alpha: Excitation strength (how much each arrival increases intensity)
|
|
||||||
beta: Decay rate (how quickly excitation fades)
|
|
||||||
side_probs: Probability distribution over BUY/SELL sides
|
|
||||||
"""
|
|
||||||
base_rate: float = 5.0
|
|
||||||
alpha: float = 0.5
|
|
||||||
beta: float = 1.0
|
|
||||||
side_probs: dict[Side, float] = None
|
|
||||||
|
|
||||||
def __post_init__(self):
|
|
||||||
if self.side_probs is None:
|
|
||||||
self.side_probs = {Side.BUY: 0.5, Side.SELL: 0.5}
|
|
||||||
|
|
||||||
class HawkesArrivalModel:
|
|
||||||
"""Self-exciting Hawkes point process for clustered arrivals.
|
|
||||||
|
|
||||||
Models order flow where arrivals cluster in time (momentum, herding).
|
|
||||||
Intensity: lambda(t) = base + alpha * sum(exp(-beta * (t - t_i)))
|
|
||||||
|
|
||||||
Used for market making scenarios where orders arrive in bursts.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cfg: HawkesArrivalConfig | None = None):
|
|
||||||
self.cfg = cfg or HawkesArrivalConfig()
|
|
||||||
self._history: np.ndarray = np.array([])
|
|
||||||
|
|
||||||
def sample(self, t: float, dt: float, instruments: InstrumentSet,
|
|
||||||
market: MarketState | None, hidden: HiddenState,
|
|
||||||
rng: np.random.Generator) -> list[Opportunity]:
|
|
||||||
intensity = hawkes_intensity(
|
|
||||||
self.cfg.base_rate * hidden.true_demand_intensity,
|
|
||||||
self._history, self.cfg.alpha, self.cfg.beta, t
|
|
||||||
)
|
|
||||||
n_arrivals = poisson_arrivals(intensity, dt, rng)
|
|
||||||
opps = []
|
|
||||||
for i in range(n_arrivals):
|
|
||||||
arr_t = t + rng.uniform(0, dt)
|
|
||||||
self._history = np.append(self._history, arr_t)
|
|
||||||
inst_id = rng.integers(0, instruments.n)
|
|
||||||
side = rng.choice(list(self.cfg.side_probs.keys()),
|
|
||||||
p=list(self.cfg.side_probs.values()))
|
|
||||||
opps.append(Opportunity(
|
|
||||||
id=str(uuid4())[:8], type=OpportunityType.MARKET_ORDER,
|
|
||||||
side=side, instrument_id=inst_id,
|
|
||||||
size=rng.exponential(1.0), t=arr_t,
|
|
||||||
context={'intensity': intensity}
|
|
||||||
))
|
|
||||||
# decay old history
|
|
||||||
self._history = self._history[self._history > t - 10]
|
|
||||||
return opps
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SessionArrivalConfig:
|
|
||||||
"""Configuration for retail session arrivals.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
sessions_per_step: Number of browsing sessions per step
|
|
||||||
views_per_session: (min, max) product views per session
|
|
||||||
contamination: Fraction of sessions that are scrapers/bots
|
|
||||||
"""
|
|
||||||
sessions_per_step: int = 20
|
|
||||||
views_per_session: tuple[int, int] = (1, 5)
|
|
||||||
contamination: float = 0.0
|
|
||||||
|
|
||||||
class SessionArrivalModel:
|
|
||||||
"""Retail browsing session model with multi-product views.
|
|
||||||
|
|
||||||
Each session views multiple products, generating one opportunity per view.
|
|
||||||
Scraper sessions (controlled by contamination) view more products
|
|
||||||
but convert at lower rates (handled by ExecutionModel).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cfg: SessionArrivalConfig | None = None):
|
|
||||||
self.cfg = cfg or SessionArrivalConfig()
|
|
||||||
|
|
||||||
def sample(self, t: float, dt: float, instruments: InstrumentSet,
|
|
||||||
market: MarketState | None, hidden: HiddenState,
|
|
||||||
rng: np.random.Generator) -> list[Opportunity]:
|
|
||||||
n_sessions = self.cfg.sessions_per_step
|
|
||||||
contamination = hidden.contamination if hidden else self.cfg.contamination
|
|
||||||
opps = []
|
|
||||||
|
|
||||||
for _ in range(n_sessions):
|
|
||||||
is_scraper = rng.random() < contamination
|
|
||||||
n_views = rng.integers(*self.cfg.views_per_session)
|
|
||||||
sid = str(uuid4())[:8]
|
|
||||||
|
|
||||||
# scrapers view more products
|
|
||||||
if is_scraper:
|
|
||||||
n_views = min(instruments.n, n_views * 3)
|
|
||||||
|
|
||||||
viewed = rng.choice(instruments.n, size=min(n_views, instruments.n), replace=False)
|
|
||||||
for inst_id in viewed:
|
|
||||||
opps.append(Opportunity(
|
|
||||||
id=f"{sid}-{inst_id}", type=OpportunityType.SESSION,
|
|
||||||
side=Side.BUY, instrument_id=int(inst_id), size=1.0, t=t,
|
|
||||||
context={'session_id': sid, 'is_scraper': is_scraper, 'n_views': n_views}
|
|
||||||
))
|
|
||||||
return opps
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
"""
|
|
||||||
Market and competitor models for external dynamics.
|
|
||||||
|
|
||||||
This module provides models for competitor pricing (retail) and market dynamics (finance):
|
|
||||||
- StaticCompetitorModel: Fixed competitor prices
|
|
||||||
- ReactiveCompetitorModel: Competitor reacts to agent's prices, can trigger price wars
|
|
||||||
- StochasticCompetitorModel: Random walk competitor prices
|
|
||||||
- GBMMarketModel: Geometric Brownian Motion for asset mid-prices
|
|
||||||
|
|
||||||
Each model implements the MarketModel protocol.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass
|
|
||||||
import numpy as np
|
|
||||||
from ..outlet.types import Quote, MarketState, HiddenState
|
|
||||||
from ..outlet.math_util import clamp, ema
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class StaticCompetitorConfig:
|
|
||||||
"""Configuration for static competitor.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
markup: Fixed percentage markup over reference prices
|
|
||||||
"""
|
|
||||||
markup: float = 0.1
|
|
||||||
|
|
||||||
class StaticCompetitorModel:
|
|
||||||
"""Static competitor with fixed markup pricing.
|
|
||||||
|
|
||||||
Competitor prices = reference * (1 + markup).
|
|
||||||
Useful as a baseline or for testing without competitor dynamics.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cfg: StaticCompetitorConfig | None = None, refs: np.ndarray | None = None):
|
|
||||||
self.cfg = cfg or StaticCompetitorConfig()
|
|
||||||
self.refs = refs
|
|
||||||
|
|
||||||
def step(self, t: float, self_quotes: Quote, hidden: HiddenState,
|
|
||||||
rng: np.random.Generator) -> MarketState:
|
|
||||||
refs = self.refs if self.refs is not None else self_quotes.prices
|
|
||||||
comp_prices = refs * (1 + self.cfg.markup)
|
|
||||||
return MarketState(competitor_quotes=comp_prices, regime='static', t=t)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ReactiveCompetitorConfig:
|
|
||||||
"""Configuration for reactive competitor.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
follow_weight: Smoothing weight for price following (0=ignore, 1=instant)
|
|
||||||
band_pct: Maximum deviation from reference prices
|
|
||||||
war_threshold: Relative price diff that triggers price war
|
|
||||||
war_aggression: How much competitor cuts prices during war
|
|
||||||
"""
|
|
||||||
follow_weight: float = 0.3
|
|
||||||
band_pct: float = 0.1
|
|
||||||
war_threshold: float = -0.15
|
|
||||||
war_aggression: float = 0.2
|
|
||||||
|
|
||||||
class ReactiveCompetitorModel:
|
|
||||||
"""Competitor that reacts to agent's prices with price war dynamics.
|
|
||||||
|
|
||||||
The competitor follows the agent's prices with smoothing.
|
|
||||||
If the agent undercuts significantly (beyond war_threshold),
|
|
||||||
a price war is triggered where the competitor becomes more aggressive.
|
|
||||||
|
|
||||||
This creates non-stationary dynamics that test policy robustness.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cfg: ReactiveCompetitorConfig | None = None, refs: np.ndarray | None = None):
|
|
||||||
self.cfg = cfg or ReactiveCompetitorConfig()
|
|
||||||
self.refs = refs
|
|
||||||
self._prices: np.ndarray | None = None
|
|
||||||
self._in_war: bool = False
|
|
||||||
|
|
||||||
def step(self, t: float, self_quotes: Quote, hidden: HiddenState,
|
|
||||||
rng: np.random.Generator) -> MarketState:
|
|
||||||
refs = self.refs if self.refs is not None else self_quotes.prices
|
|
||||||
c = self.cfg
|
|
||||||
|
|
||||||
if self._prices is None:
|
|
||||||
self._prices = refs.copy()
|
|
||||||
|
|
||||||
# check for price war trigger
|
|
||||||
relative_diff = (self_quotes.prices - self._prices) / (self._prices + 1e-8)
|
|
||||||
if np.any(relative_diff < c.war_threshold):
|
|
||||||
self._in_war = True
|
|
||||||
elif np.all(relative_diff > -c.war_threshold / 2):
|
|
||||||
self._in_war = False
|
|
||||||
|
|
||||||
# update prices
|
|
||||||
if self._in_war:
|
|
||||||
target = self_quotes.prices * (1 - c.war_aggression)
|
|
||||||
hidden.regime = 'price_war'
|
|
||||||
else:
|
|
||||||
target = self_quotes.prices * (1 + c.follow_weight * 0.05)
|
|
||||||
hidden.regime = 'normal'
|
|
||||||
|
|
||||||
# follow with smoothing
|
|
||||||
new_prices = np.array([ema(old, new, c.follow_weight)
|
|
||||||
for old, new in zip(self._prices, target)])
|
|
||||||
|
|
||||||
# stay within band
|
|
||||||
new_prices = clamp(new_prices, refs * (1 - c.band_pct), refs * (1 + c.band_pct))
|
|
||||||
self._prices = new_prices
|
|
||||||
|
|
||||||
return MarketState(competitor_quotes=new_prices, regime=hidden.regime, t=t)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class StochasticCompetitorConfig:
|
|
||||||
"""Configuration for stochastic competitor.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
drift: Price drift per step
|
|
||||||
volatility: Price volatility (std of random shocks)
|
|
||||||
mean_revert: Mean reversion strength toward reference
|
|
||||||
"""
|
|
||||||
drift: float = 0.0
|
|
||||||
volatility: float = 0.02
|
|
||||||
mean_revert: float = 0.1
|
|
||||||
|
|
||||||
class StochasticCompetitorModel:
|
|
||||||
"""Ornstein-Uhlenbeck style stochastic competitor prices.
|
|
||||||
|
|
||||||
Prices follow: dP = drift + mean_revert*(ref - P) + volatility*P*dW
|
|
||||||
|
|
||||||
Provides non-stationary competitor dynamics independent of agent actions.
|
|
||||||
Useful for testing robustness to market noise.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cfg: StochasticCompetitorConfig | None = None, refs: np.ndarray | None = None):
|
|
||||||
self.cfg = cfg or StochasticCompetitorConfig()
|
|
||||||
self.refs = refs
|
|
||||||
self._prices: np.ndarray | None = None
|
|
||||||
|
|
||||||
def step(self, t: float, self_quotes: Quote, hidden: HiddenState,
|
|
||||||
rng: np.random.Generator) -> MarketState:
|
|
||||||
refs = self.refs if self.refs is not None else self_quotes.prices
|
|
||||||
c = self.cfg
|
|
||||||
|
|
||||||
if self._prices is None:
|
|
||||||
self._prices = refs.copy()
|
|
||||||
|
|
||||||
# Ornstein-Uhlenbeck style dynamics
|
|
||||||
n = len(self._prices)
|
|
||||||
noise = rng.normal(0, c.volatility, n)
|
|
||||||
reversion = c.mean_revert * (refs - self._prices)
|
|
||||||
self._prices = self._prices + c.drift + reversion + noise * self._prices
|
|
||||||
self._prices = np.maximum(self._prices, refs * 0.5)
|
|
||||||
|
|
||||||
return MarketState(competitor_quotes=self._prices.copy(), regime='stochastic', t=t)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class GBMMarketConfig:
|
|
||||||
"""Configuration for GBM market model.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
mu: Price drift (expected return)
|
|
||||||
sigma: Price volatility
|
|
||||||
dt: Time step size
|
|
||||||
"""
|
|
||||||
mu: float = 0.0
|
|
||||||
sigma: float = 0.1
|
|
||||||
dt: float = 1.0
|
|
||||||
|
|
||||||
class GBMMarketModel:
|
|
||||||
"""Geometric Brownian Motion model for asset mid-prices.
|
|
||||||
|
|
||||||
Standard Black-Scholes dynamics: dS = mu*S*dt + sigma*S*dW
|
|
||||||
|
|
||||||
Used for market making scenarios where the underlying asset price
|
|
||||||
follows a random walk. The agent quotes around this moving mid-price.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cfg: GBMMarketConfig | None = None, initial: np.ndarray | None = None):
|
|
||||||
self.cfg = cfg or GBMMarketConfig()
|
|
||||||
self._mids = initial
|
|
||||||
|
|
||||||
def step(self, t: float, self_quotes: Quote, hidden: HiddenState,
|
|
||||||
rng: np.random.Generator) -> MarketState:
|
|
||||||
if self._mids is None:
|
|
||||||
self._mids = self_quotes.prices.copy()
|
|
||||||
|
|
||||||
c = self.cfg
|
|
||||||
n = len(self._mids)
|
|
||||||
z = rng.standard_normal(n)
|
|
||||||
self._mids = self._mids * np.exp((c.mu - 0.5*c.sigma**2)*c.dt + c.sigma*np.sqrt(c.dt)*z)
|
|
||||||
|
|
||||||
vol = np.full(n, c.sigma)
|
|
||||||
return MarketState(mid_prices=self._mids.copy(), volatility=vol, regime='gbm', t=t)
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
"""
|
|
||||||
Execution models for computing acceptance/fill probabilities.
|
|
||||||
|
|
||||||
This module provides different models for how opportunities convert to executions:
|
|
||||||
- ElasticityExecutionModel: Price elasticity with competitor cross-effects (retail)
|
|
||||||
- IntensityExecutionModel: Distance-based fill intensity (market making)
|
|
||||||
- LogitExecutionModel: Discrete choice model
|
|
||||||
|
|
||||||
Each model implements the ExecutionModel protocol.
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from typing import Any
|
|
||||||
import numpy as np
|
|
||||||
from ..outlet.types import Opportunity, Quote, InstrumentSet, MarketState
|
|
||||||
from ..outlet.constants import Side
|
|
||||||
from ..outlet.math_util import sigmoid, safe_log, intensity_decay, EPS
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class ElasticityConfig:
|
|
||||||
"""Configuration for price elasticity execution model.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
base_prob: Baseline purchase probability at reference price
|
|
||||||
price_sensitivity: Own-price elasticity coefficient
|
|
||||||
cross_elasticity: Competitor price cross-elasticity
|
|
||||||
scraper_conversion: Multiplier for scraper conversion (typically << 1)
|
|
||||||
"""
|
|
||||||
base_prob: float = 0.3
|
|
||||||
price_sensitivity: float = 2.0
|
|
||||||
cross_elasticity: float = 0.5
|
|
||||||
scraper_conversion: float = 0.01
|
|
||||||
|
|
||||||
class ElasticityExecutionModel:
|
|
||||||
"""Price elasticity model for retail dynamic pricing.
|
|
||||||
|
|
||||||
P(buy) = base_prob * exp(-sensitivity * log(price/ref)) * cross_effect * scraper_mult
|
|
||||||
|
|
||||||
Higher prices reduce purchase probability exponentially.
|
|
||||||
Competitor undercutting shifts demand away from the platform.
|
|
||||||
Scrapers convert at a much lower rate (reconnaissance, not purchase).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cfg: ElasticityConfig | None = None):
|
|
||||||
self.cfg = cfg or ElasticityConfig()
|
|
||||||
|
|
||||||
def prob(self, opp: Opportunity, quote: Quote, instruments: InstrumentSet,
|
|
||||||
market: MarketState | None, rng: np.random.Generator) -> float:
|
|
||||||
idx = int(opp.instrument_id)
|
|
||||||
price = quote.prices[idx]
|
|
||||||
ref = instruments.refs[idx]
|
|
||||||
|
|
||||||
# base probability adjusted by price ratio
|
|
||||||
log_ratio = safe_log(price / ref)
|
|
||||||
prob = self.cfg.base_prob * np.exp(-self.cfg.price_sensitivity * log_ratio)
|
|
||||||
|
|
||||||
# cross-elasticity: competitor undercutting increases their share
|
|
||||||
if market and market.competitor_quotes is not None:
|
|
||||||
comp_price = market.competitor_quotes[idx]
|
|
||||||
if comp_price < price:
|
|
||||||
prob *= np.exp(-self.cfg.cross_elasticity * (price - comp_price) / ref)
|
|
||||||
|
|
||||||
# scrapers convert at much lower rate
|
|
||||||
if opp.context.get('is_scraper', False):
|
|
||||||
prob *= self.cfg.scraper_conversion
|
|
||||||
|
|
||||||
return float(np.clip(prob, 0, 1))
|
|
||||||
|
|
||||||
def uncensor(self, fills: np.ndarray, instruments: InstrumentSet,
|
|
||||||
context: dict[str, Any] | None = None) -> np.ndarray:
|
|
||||||
# simple imputation: assume fills = prob * exposures, invert
|
|
||||||
exposures = context.get('exposures', fills) if context else fills
|
|
||||||
avg_prob = self.cfg.base_prob
|
|
||||||
return fills / (avg_prob + EPS)
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class IntensityConfig:
|
|
||||||
"""Configuration for intensity-based execution model.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
base_intensity: Baseline fill intensity
|
|
||||||
kappa: Decay rate with distance from mid-price
|
|
||||||
vol_scale: Volatility multiplier for fill intensity
|
|
||||||
"""
|
|
||||||
base_intensity: float = 1.0
|
|
||||||
kappa: float = 1.5
|
|
||||||
vol_scale: float = 0.5
|
|
||||||
|
|
||||||
class IntensityExecutionModel:
|
|
||||||
"""Avellaneda-Stoikov style fill intensity for market making.
|
|
||||||
|
|
||||||
Fill probability decays exponentially with distance from mid-price:
|
|
||||||
P(fill) = base * exp(-kappa * |quote - mid|) * (1 + vol_scale * sigma)
|
|
||||||
|
|
||||||
Tighter spreads (closer to mid) have higher fill probability.
|
|
||||||
Higher volatility increases fill probability (more aggressive traders).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cfg: IntensityConfig | None = None):
|
|
||||||
self.cfg = cfg or IntensityConfig()
|
|
||||||
|
|
||||||
def prob(self, opp: Opportunity, quote: Quote, instruments: InstrumentSet,
|
|
||||||
market: MarketState | None, rng: np.random.Generator) -> float:
|
|
||||||
idx = int(opp.instrument_id)
|
|
||||||
|
|
||||||
# get mid price from market or use quote price
|
|
||||||
if market and market.mid_prices is not None:
|
|
||||||
mid = market.mid_prices[idx]
|
|
||||||
else:
|
|
||||||
mid = quote.prices[idx]
|
|
||||||
|
|
||||||
# compute distance from mid
|
|
||||||
if opp.side == Side.BUY:
|
|
||||||
exec_price = quote.asks[idx] if quote.asks is not None else quote.prices[idx]
|
|
||||||
distance = exec_price - mid
|
|
||||||
else:
|
|
||||||
exec_price = quote.bids[idx] if quote.bids is not None else quote.prices[idx]
|
|
||||||
distance = mid - exec_price
|
|
||||||
|
|
||||||
# intensity decays with distance
|
|
||||||
intensity = self.cfg.base_intensity * intensity_decay(abs(distance), self.cfg.kappa)
|
|
||||||
|
|
||||||
# volatility increases fill probability
|
|
||||||
if market and market.volatility is not None:
|
|
||||||
vol = market.volatility[idx]
|
|
||||||
intensity *= (1 + self.cfg.vol_scale * vol)
|
|
||||||
|
|
||||||
return float(np.clip(intensity, 0, 1))
|
|
||||||
|
|
||||||
def uncensor(self, fills: np.ndarray, instruments: InstrumentSet,
|
|
||||||
context: dict[str, Any] | None = None) -> np.ndarray:
|
|
||||||
return fills # market making doesn't have same censorship concept
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class LogitConfig:
|
|
||||||
"""Configuration for logit discrete choice model.
|
|
||||||
|
|
||||||
Attributes:
|
|
||||||
beta_0: Intercept (base utility)
|
|
||||||
beta_price: Price coefficient (typically negative)
|
|
||||||
beta_quality: Quality attribute coefficient
|
|
||||||
"""
|
|
||||||
beta_0: float = 0.5
|
|
||||||
beta_price: float = -1.5
|
|
||||||
beta_quality: float = 0.3
|
|
||||||
|
|
||||||
class LogitExecutionModel:
|
|
||||||
"""Discrete choice logit model for purchase probability.
|
|
||||||
|
|
||||||
Utility: U = beta_0 + beta_price * (price/ref) + beta_quality * quality
|
|
||||||
P(buy) = sigmoid(U)
|
|
||||||
|
|
||||||
Provides a theoretically grounded demand model from economics literature.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, cfg: LogitConfig | None = None):
|
|
||||||
self.cfg = cfg or LogitConfig()
|
|
||||||
|
|
||||||
def prob(self, opp: Opportunity, quote: Quote, instruments: InstrumentSet,
|
|
||||||
market: MarketState | None, rng: np.random.Generator) -> float:
|
|
||||||
idx = int(opp.instrument_id)
|
|
||||||
price = quote.prices[idx]
|
|
||||||
ref = instruments.refs[idx]
|
|
||||||
quality = instruments.instruments[idx].attrs.get('quality', 0.5)
|
|
||||||
|
|
||||||
# utility
|
|
||||||
u = self.cfg.beta_0 + self.cfg.beta_price * (price / ref) + self.cfg.beta_quality * quality
|
|
||||||
|
|
||||||
# choice probability via sigmoid
|
|
||||||
return float(sigmoid(u))
|
|
||||||
|
|
||||||
def uncensor(self, fills: np.ndarray, instruments: InstrumentSet,
|
|
||||||
context: dict[str, Any] | None = None) -> np.ndarray:
|
|
||||||
return fills / (self.cfg.beta_0 + EPS)
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
"""Example script demonstrating the Quote-Control platform"""
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
|
||||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
||||||
|
|
||||||
import numpy as np
|
|
||||||
from lab.config import make_retail_platform, make_market_making_platform
|
|
||||||
from lab.experiments.eval import (rollout, compare_policies, fixed_price_policy,
|
|
||||||
cost_plus_margin_policy, random_walk_policy)
|
|
||||||
|
|
||||||
def demo_retail():
|
|
||||||
print("=" * 60)
|
|
||||||
print("RETAIL DYNAMIC PRICING DEMO")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
platform = make_retail_platform()
|
|
||||||
print(f"Instruments: {platform.instruments.n}")
|
|
||||||
print(f"Reference prices: {platform.instruments.refs[:5].round(2)}...")
|
|
||||||
|
|
||||||
# compare policies
|
|
||||||
policies = {
|
|
||||||
'fixed': fixed_price_policy(platform.instruments.refs),
|
|
||||||
'cost_plus_30%': cost_plus_margin_policy(platform.instruments.costs, 0.3),
|
|
||||||
'cost_plus_50%': cost_plus_margin_policy(platform.instruments.costs, 0.5),
|
|
||||||
'random_walk': random_walk_policy(platform.instruments.refs, 0.03),
|
|
||||||
}
|
|
||||||
|
|
||||||
results = compare_policies(platform, policies, n_steps=100, n_runs=3)
|
|
||||||
|
|
||||||
print("\nPolicy Comparison (100 steps, 3 runs):")
|
|
||||||
print("-" * 50)
|
|
||||||
for name, r in sorted(results.items(), key=lambda x: -x[1]['mean_pnl']):
|
|
||||||
print(f"{name:20s} PnL={r['mean_pnl']:8.1f} +/- {r['std_reward']:6.1f} "
|
|
||||||
f"conv={r['mean_conversion']:.3f}")
|
|
||||||
|
|
||||||
def demo_market_making():
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("MARKET MAKING DEMO")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
platform = make_market_making_platform()
|
|
||||||
print(f"Instruments: {platform.instruments.n}")
|
|
||||||
print(f"Initial mids: {platform.instruments.refs.round(2)}")
|
|
||||||
|
|
||||||
# simple policy: quote at mid with fixed spread
|
|
||||||
def mm_policy(obs: np.ndarray, t: int):
|
|
||||||
mids = platform.instruments.refs # would use obs in real policy
|
|
||||||
return mids, 1.0
|
|
||||||
|
|
||||||
result = rollout(platform, mm_policy, n_steps=200, seed=42)
|
|
||||||
print(f"\nRollout (200 steps):")
|
|
||||||
print(f" Total PnL: {result.total_pnl:.2f}")
|
|
||||||
print(f" Avg conversion: {result.avg_conversion:.3f}")
|
|
||||||
print(f" Total spread capture: {sum(m.spread_capture for m in result.metrics):.2f}")
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
demo_retail()
|
|
||||||
demo_market_making()
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
$pdf_mode = 1;
|
$pdf_mode = 1;
|
||||||
$pdflatex = 'pdflatex -synctex=1 -interaction=nonstopmode -file-line-error %O %S';
|
$pdflatex = 'pdflatex -synctex=1 -interaction=nonstopmode -file-line-error %O %S';
|
||||||
$aux_dir = 'build';
|
$bibtex_use = 2; # run bibtex when needed
|
||||||
$out_dir = 'build';
|
|
||||||
$use_biber = 0; # force bibtex
|
|
||||||
$bibtex = 'bibtex %O %B';
|
$bibtex = 'bibtex %O %B';
|
||||||
$pdf_previewer = 'zathura %O %S';
|
$pdf_previewer = 'zathura %O %S';
|
||||||
$clean_ext = 'synctex.gz bbl bcf run.xml fls fdb_latexmk glg glo gls ist blg lof lot out toc';
|
$clean_ext = 'synctex.gz bbl bcf run.xml fls fdb_latexmk glg glo gls ist blg lof lot out toc';
|
||||||
|
|||||||
@@ -42,23 +42,27 @@ EOF
|
|||||||
# Process each directory
|
# Process each directory
|
||||||
echo "Concatenating code from source directories..."
|
echo "Concatenating code from source directories..."
|
||||||
|
|
||||||
|
# Engine
|
||||||
|
find "$PROJECT_ROOT/engine" -type d \( -name ".venv" -o -name "__pycache__" -o -name "*.egg-info" -o -name "node_modules" -o -name ".pytest_cache" \) -prune -o -type f \( -name "*.py" -o -name "*.js" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" \) ! -name "*.pyc" ! -name "*.pyo" -print | sort | while read -r file; do
|
||||||
|
add_file "$file"
|
||||||
|
done
|
||||||
# Backend
|
# Backend
|
||||||
find "$PROJECT_ROOT/backend" -type f \( -name "*.py" -o -name "*.js" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" \) | sort | while read -r file; do
|
find "$PROJECT_ROOT/backend" -type d \( -name ".venv" -o -name "__pycache__" -o -name "*.egg-info" -o -name "node_modules" -o -name ".pytest_cache" \) -prune -o -type f \( -name "*.py" -o -name "*.js" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" \) ! -name "*.pyc" ! -name "*.pyo" -print | sort | while read -r file; do
|
||||||
add_file "$file"
|
add_file "$file"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Experiments
|
# Experiments
|
||||||
find "$PROJECT_ROOT/experiments" -type f \( -name "*.py" -o -name "*.js" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" \) | sort | while read -r file; do
|
find "$PROJECT_ROOT/experiments" -type d \( -name ".venv" -o -name "__pycache__" -o -name "*.egg-info" -o -name "node_modules" -o -name ".pytest_cache" -o -name ".ipynb_checkpoints" \) -prune -o -type f \( -name "*.py" -o -name "*.js" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" \) ! -name "*.pyc" ! -name "*.pyo" -print | sort | while read -r file; do
|
||||||
add_file "$file"
|
add_file "$file"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
find "$PROJECT_ROOT/docker" -type f \( -name "*.py" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" -o -name "Dockerfile*" \) | sort | while read -r file; do
|
find "$PROJECT_ROOT/docker" -type d \( -name ".venv" -o -name "__pycache__" -o -name "node_modules" \) -prune -o -type f \( -name "*.py" -o -name "*.sh" -o -name "*.yml" -o -name "*.yaml" -o -name "*.Dockerfile*" \) ! -name "*.pyc" ! -name "*.pyo" -print | sort | while read -r file; do
|
||||||
add_file "$file"
|
add_file "$file"
|
||||||
done
|
done
|
||||||
|
|
||||||
# Web/src
|
# Web/src
|
||||||
find "$PROJECT_ROOT/web/src" -type f \( -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" \) | sort | while read -r file; do
|
find "$PROJECT_ROOT/web/src" -type d \( -name "node_modules" -o -name ".next" -o -name "dist" -o -name "build" \) -prune -o -type f \( -name "*.js" -o -name "*.jsx" -o -name "*.ts" -o -name "*.tsx" \) -print | sort | while read -r file; do
|
||||||
add_file "$file"
|
add_file "$file"
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
(setq TeX-command-extra-options
|
(setq TeX-command-extra-options
|
||||||
"-file-line-error -interaction=nonstopmode")
|
"-file-line-error -interaction=nonstopmode")
|
||||||
(TeX-add-to-alist 'LaTeX-provided-class-options
|
(TeX-add-to-alist 'LaTeX-provided-class-options
|
||||||
'(("report" "12pt") ("article" "12pt") ("acmart" "sigconf" "nonacm" "natbib=false")))
|
'(("report" "12pt") ("acmart" "sigconf" "nonacm" "natbib=false" "manuscript") ("article" "12pt" "letterpaper")))
|
||||||
(TeX-run-style-hooks
|
(TeX-run-style-hooks
|
||||||
"latex2e"
|
"latex2e"
|
||||||
"preamble"
|
"preamble"
|
||||||
@@ -16,9 +16,7 @@
|
|||||||
"chapters/04-results"
|
"chapters/04-results"
|
||||||
"chapters/05-discussion"
|
"chapters/05-discussion"
|
||||||
"chapters/06-conclusion"
|
"chapters/06-conclusion"
|
||||||
"../build/concatenated_code"
|
"article"
|
||||||
"acmart"
|
"art12"))
|
||||||
"acmart10")
|
|
||||||
(TeX-add-symbols
|
|
||||||
'("footnotetextcopyrightpermission" 1)))
|
|
||||||
:latex)
|
:latex)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,618 @@
|
|||||||
|
|
||||||
|
@article{arnoud_v_den_boer_dynamic_2015,
|
||||||
|
title = {Dynamic pricing and learning: {Historical} origins, current research, and new directions},
|
||||||
|
volume = {20},
|
||||||
|
url = {https://www.sciencedirect.com/science/article/pii/S1876735415000021},
|
||||||
|
doi = {10.1016/j.sorms.2015.03.001},
|
||||||
|
number = {1},
|
||||||
|
journal = {Surveys in Operations Research and Management Science},
|
||||||
|
author = {{Arnoud V. den Boer}},
|
||||||
|
month = jun,
|
||||||
|
year = {2015},
|
||||||
|
pages = {1--18},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/NUAGDYER/memo2025.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{iliou_detection_2021,
|
||||||
|
title = {Detection of {Advanced} {Web} {Bots} by {Combining} {Web} {Logs} with {Mouse} {Behavioural} {Biometrics}},
|
||||||
|
volume = {2},
|
||||||
|
url = {https://dl.acm.org/doi/10.1145/3447815},
|
||||||
|
doi = {10.1145/3447815},
|
||||||
|
number = {3},
|
||||||
|
journal = {Digital Threats: Research and Practice},
|
||||||
|
author = {Iliou, Christos and Kostoulas, Theodoros and Tsikrika, Theodora and Katos, Vasilis and Vrochidis, Stefanos and Kompatsiaris, Ioannis},
|
||||||
|
year = {2021},
|
||||||
|
pages = {1--26},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/Q7J5EBEJ/3447815.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@phdthesis{salassa_politecnico_2024,
|
||||||
|
title = {Politecnico di {Torino} {Algorithmic} {Pricing} in the digital age "{Ethical} considerations on its economic and social implications, and an analysis of possible solutions to overcome its critical issues" {Tutor}: {Candidate}},
|
||||||
|
abstract = {Algorithmic pricing is an emerging business practice that uses computational algorithms to determine
|
||||||
|
the prices of products and services based on a number of dynamic factors. The aim of this thesis is to
|
||||||
|
draw attention to the existence of these business practices, and the ethical and social implications that
|
||||||
|
derive from them, and then focus on what could be effective solutions to increase the well-being of
|
||||||
|
the community.
|
||||||
|
In Chapter 2 of the thesis, a general introduction to the topic will be made, starting from its history
|
||||||
|
and its evolution over the years; Chapter 3 will examine the different types of pricing algorithms.
|
||||||
|
Subsequently, in Chapter 4 we will analyze the sectors in which they are most applicable, and the
|
||||||
|
relative advantages and disadvantages they bring with them, with a critical analysis of the trade-offs
|
||||||
|
generated. The effect of algorithmic pricing on competition will be studied, considering how the
|
||||||
|
ability of algorithms to adapt quickly to market conditions can foster anti-competitive practices, such
|
||||||
|
as price discrimination. Later, in Chapter 5, we will look at the issue of price transparency and how
|
||||||
|
the opacity of algorithms can make it difficult for consumers to understand the pricing process and
|
||||||
|
assess whether they are receiving fair treatment.
|
||||||
|
To address these ethical issues, several possible solutions will be brought to light, described in
|
||||||
|
Chapter 6, which will focus on the role of the government, as a regulatory, of the end consumer, who
|
||||||
|
must be encouraged to educate and inform himself about the use of these practices, and of the
|
||||||
|
company, as responsible for making its customers aware and acting in compliance with government
|
||||||
|
laws, for fair and non-discriminatory use.},
|
||||||
|
urldate = {2025-11-12},
|
||||||
|
school = {Politecnico di Torino},
|
||||||
|
author = {Salassa, Fabio and Pautassi, Paolo},
|
||||||
|
month = apr,
|
||||||
|
year = {2024},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/L95WYQ8B/m-api-06aad998-d926-0d59-5593-82fdce5a678b.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@inproceedings{mueller_low-rank_2019,
|
||||||
|
title = {Low-{Rank} {Bandit} {Methods} for {High}-{Dimensional} {Dynamic} {Pricing}},
|
||||||
|
booktitle = {Advances in {Neural} {Information} {Processing} {Systems} 32 ({NeurIPS} 2019)},
|
||||||
|
author = {Mueller, Jonas W and Syrgkanis, Vasilis and Taddy, Matt},
|
||||||
|
year = {2019},
|
||||||
|
pages = {15442--15452},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/IZD3C5SR/m-api-26f6207c-cc89-4aed-29b6-34629f18fe9b.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{shahidi_coasean_2025,
|
||||||
|
title = {The {Coasean} {Singularity}? {Demand}, {Supply}, and {Market} {Design} with {AI} {Agents}},
|
||||||
|
abstract = {AI agents—autonomous systems that perceive, reason, and act on behalf of human principals—are poised to transform digital markets by dramatically reducing transaction costs. This chapter evaluates the economic implications of this transition, adopting a consumeroriented view of agents as market participants that can search, negotiate, and transact directly. From the demand side, agent adoption reflects derived demand: users trade off decision quality against effort reduction, with outcomes mediated by agent capability and task context. On the supply side, firms will design, integrate, and monetize agents, with outcomes hinging on whether agents operate within or across platforms. At the market level, agents create efficiency gains from lower search, communication, and contracting costs, but also introduce frictions such as congestion and price obfuscation. By lowering the costs of preference elicitation, contract enforcement, and identity verification, agents expand the feasible set of market designs but also raise novel regulatory challenges. While the net welfare effects remain an empirical question, the rapid onset of AI-mediated transactions presents a unique opportunity for economic research to inform real-world policy and market design.},
|
||||||
|
language = {en},
|
||||||
|
author = {Shahidi, Peyman and Rusak, Gili and Manning, Benjamin S and Fradkin, Andrey and Horton, John J},
|
||||||
|
year = {2025},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/TQCAPJDP/Shahidi et al. - The Coasean Singularity Demand, Supply, and Market Design with AI Agents.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{byrnes_intro_2025,
|
||||||
|
title = {Intro to {Brain}-{Like}-{AGI} {Safety}},
|
||||||
|
url = {https://osf.io/fe36n_v1},
|
||||||
|
doi = {10.31219/osf.io/fe36n_v1},
|
||||||
|
abstract = {Suppose we someday build an Artificial General Intelligence (AGI) algorithm using similar principles of learning and cognition as the human brain. How would we use such an algorithm safely? I argue that this is an open technical problem, and my goal is to bring readers with no prior knowledge all the way up to the front-line of unsolved problems. Chapter 1 has background and motivation; Chapters 2-7 are on neuroscience, arguing for a picture of the brain that combines large-scale learning algorithms (e.g. in the cortex) and specific evolved reflexes (e.g. in the hypothalamus and brainstem); and Chapters 8-15 apply those neuroscience ideas to AGI safety. A major theme is the idea that the brain has something like a reinforcement learning reward function, which says that pain is bad, eating-when-hungry is good, etc. I argue that this reward function is centered around the hypothalamus and brainstem, and that all human desires—even "higher" desires for things like compassion and justice—come directly or indirectly from that innate reward function. If future programmers build brain-like AGI, they will likewise have a reward function slot in their source code, in which they can put whatever they want. If they put the wrong thing, the resulting AGI will wind up callously indifferent to human welfare. How might they avoid that? That's an open technical problem, but I will review some ideas and research directions.},
|
||||||
|
language = {en},
|
||||||
|
urldate = {2025-12-31},
|
||||||
|
publisher = {Open Science Framework},
|
||||||
|
author = {Byrnes, Steven J.},
|
||||||
|
month = mar,
|
||||||
|
year = {2025},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/ZLJQ4DQ9/Byrnes - 2025 - Intro to Brain-Like-AGI Safety.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{shannon_mathematical_1948,
|
||||||
|
title = {A {Mathematical} {Theory} of {Communication}},
|
||||||
|
volume = {27},
|
||||||
|
language = {en},
|
||||||
|
journal = {Bell System Technical Journal},
|
||||||
|
author = {Shannon, C E},
|
||||||
|
month = oct,
|
||||||
|
year = {1948},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/FJRFRWK2/Shannon - A Mathematical Theory of Communication.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{noauthor_order_stats_nodate,
|
||||||
|
title = {order\_stats},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/D3QRGY9Z/order_stats.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{devine_nonlinear_2017,
|
||||||
|
title = {Nonlinear {Pricing} with {Costly} {Information} {Acquisition}},
|
||||||
|
abstract = {This paper examines a nonlinear pricing model where the firm can choose to acquire costly information prior to offering contract menus to consumers; such as paying a consultant or investing in machine learning technologies. Information provides the firm with a signal about consumers types, whose accuracy increases as the firm acquires larger amounts of information. We show that the firm chooses to acquire information, only if it can purchase a sufficient amount that could alter its initial prior beliefs. Relative to standard settings where firms cannot acquire information, we identify how information acquisition changes optimal contract offers, equilibrium profits, information rents, and welfare. A better-informed firm increases its expected profits, but it can also increase expected utility when the cost of information is intermediate. Our results recommend balanced online privacy laws.},
|
||||||
|
language = {en},
|
||||||
|
author = {Devine, Brett R and Munoz-Garcia, Felix},
|
||||||
|
month = nov,
|
||||||
|
year = {2017},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/GQ28KVBF/Devine and Munoz-Garcia - Nonlinear Pricing with Costly Information Acquisition.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{wang_learning_2025,
|
||||||
|
title = {Learning {Optimal} {Distributionally} {Robust} {Stochastic} {Control} in {Continuous} {State} {Spaces}},
|
||||||
|
url = {http://arxiv.org/abs/2406.11281},
|
||||||
|
doi = {10.48550/arXiv.2406.11281},
|
||||||
|
abstract = {We study data-driven learning of robust stochastic control for infinite-horizon systems with potentially continuous state and action spaces. In many managerial settings–supply chains, finance, manufacturing, services, and dynamic games–the state-transition mechanism is determined by system design, while available data capture the distributional properties of the stochastic inputs from the environment. For modeling and computational tractability, a decision maker often adopts a Markov control model with i.i.d. environment inputs, which can render learned policies fragile to internal dependence or external perturbations. We introduce a distributionally robust stochastic control paradigm that promotes policy reliability by introducing adaptive adversarial perturbations to the environment input, while preserving the modeling, statistical, and computational tractability of the Markovian formulation. From a modeling perspective, we examine two adversary models–current-action-aware and current-action-unaware–leading to distinct dynamic behaviors and robust optimal policies. From a statistical learning perspective, we characterize optimal finite-sample minimax rates for uniform learning of the robust value function across a continuum of states under ambiguity sets defined by the fk-divergence and Wasserstein distance. To efficiently compute the optimal robust policies, we further propose algorithms inspired by deep reinforcement learning methodologies. Finally, we demonstrate the applicability of the framework to real managerial problems.},
|
||||||
|
language = {en},
|
||||||
|
urldate = {2025-12-29},
|
||||||
|
publisher = {arXiv},
|
||||||
|
author = {Wang, Shengbo and Meng, Jason and Si, Nian and Blanchet, Jose and Zhou, Zhengyuan},
|
||||||
|
month = nov,
|
||||||
|
year = {2025},
|
||||||
|
note = {arXiv:2406.11281 [stat]},
|
||||||
|
keywords = {Computer Science - Machine Learning, Statistics - Machine Learning},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/RQ8XDSSG/Wang et al. - 2025 - Learning Optimal Distributionally Robust Stochastic Control in Continuous State Spaces.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{ie_recsim_2019,
|
||||||
|
title = {{RecSim}: {A} {Configurable} {Simulation} {Platform} for {Recommender} {Systems}},
|
||||||
|
shorttitle = {{RecSim}},
|
||||||
|
url = {http://arxiv.org/abs/1909.04847},
|
||||||
|
doi = {10.48550/arXiv.1909.04847},
|
||||||
|
abstract = {We propose RecSim, a configurable platform for authoring simulation environments for recommender systems (RSs) that naturally supports sequential interaction with users. RecSim allows the creation of new environments that reflect particular aspects of user behavior and item structure at a level of abstraction well-suited to pushing the limits of current reinforcement learning (RL) and RS techniques in sequential interactive recommendation problems. Environments can be easily configured that vary assumptions about: user preferences and item familiarity; user latent state and its dynamics; and choice models and other user response behavior. We outline how RecSim offers value to RL and RS researchers and practitioners, and how it can serve as a vehicle for academic-industrial collaboration.},
|
||||||
|
urldate = {2025-12-29},
|
||||||
|
publisher = {arXiv},
|
||||||
|
author = {Ie, Eugene and Hsu, Chih-wei and Mladenov, Martin and Jain, Vihan and Narvekar, Sanmit and Wang, Jing and Wu, Rui and Boutilier, Craig},
|
||||||
|
month = sep,
|
||||||
|
year = {2019},
|
||||||
|
note = {arXiv:1909.04847 [cs]},
|
||||||
|
keywords = {Computer Science - Machine Learning, Statistics - Machine Learning, Computer Science - Human-Computer Interaction, Computer Science - Information Retrieval},
|
||||||
|
file = {Preprint PDF:/home/velocitatem/Zotero/storage/CJJI2VQF/Ie et al. - 2019 - RecSim A Configurable Simulation Platform for Recommender Systems.pdf:application/pdf;Snapshot:/home/velocitatem/Zotero/storage/8XJKJTHE/1909.html:text/html},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{kuhn_wasserstein_2024,
|
||||||
|
title = {Wasserstein {Distributionally} {Robust} {Optimization}: {Theory} and {Applications} in {Machine} {Learning}},
|
||||||
|
shorttitle = {Wasserstein {Distributionally} {Robust} {Optimization}},
|
||||||
|
url = {http://arxiv.org/abs/1908.08729},
|
||||||
|
doi = {10.48550/arXiv.1908.08729},
|
||||||
|
abstract = {Many decision problems in science, engineering and economics are affected by uncertain parameters whose distribution is only indirectly observable through samples. The goal of data-driven decision-making is to learn a decision from finitely many training samples that will perform well on unseen test samples. This learning task is difficult even if all training and test samples are drawn from the same distribution—especially if the dimension of the uncertainty is large relative to the training sample size. Wasserstein distributionally robust optimization seeks data-driven decisions that perform well under the most adverse distribution within a certain Wasserstein distance from a nominal distribution constructed from the training samples. In this tutorial we will argue that this approach has many conceptual and computational benefits. Most prominently, the optimal decisions can often be computed by solving tractable convex optimization problems, and they enjoy rigorous out-of-sample and asymptotic consistency guarantees. We will also show that Wasserstein distributionally robust optimization has interesting ramifications for statistical learning and motivates new approaches for fundamental learning tasks such as classification, regression, maximum likelihood estimation or minimum mean square error estimation, among others.},
|
||||||
|
language = {en},
|
||||||
|
urldate = {2025-12-27},
|
||||||
|
publisher = {arXiv},
|
||||||
|
author = {Kuhn, Daniel and Esfahani, Peyman Mohajerin and Nguyen, Viet Anh and Shafieezadeh-Abadeh, Soroosh},
|
||||||
|
month = nov,
|
||||||
|
year = {2024},
|
||||||
|
note = {arXiv:1908.08729 [stat]},
|
||||||
|
keywords = {Computer Science - Machine Learning, Statistics - Machine Learning, Mathematics - Optimization and Control},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/FAWJEK6J/Kuhn et al. - 2024 - Wasserstein Distributionally Robust Optimization Theory and Applications in Machine Learning.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{arunachaleswaran_learning_2025,
|
||||||
|
title = {Learning to {Play} {Against} {Unknown} {Opponents}},
|
||||||
|
url = {http://arxiv.org/abs/2412.18297},
|
||||||
|
doi = {10.48550/arXiv.2412.18297},
|
||||||
|
abstract = {We consider the problem of a learning agent who has to repeatedly play a general sum game against a strategic opponent who acts to maximize their own payoff by optimally responding against the learner’s algorithm. The learning agent knows their own payoff function, but is uncertain about the payoff of their opponent (knowing only that it is drawn from some distribution D). What learning algorithm should the agent run in order to maximize their own total utility, either in expectation or in the worst-case over D? When the learning algorithm is constrained to be a no-regret algorithm, we demonstrate how to efficiently construct an optimal learning algorithm (asymptotically achieving the optimal utility) in polynomial time for both the in-expectation and worst-case problems, independent of any other assumptions. When the learning algorithm is not constrained to no-regret, we show how to construct an ε-optimal learning algorithm (obtaining average utility within ε of the optimal utility) for both the in-expectation and worst-case problems in time polynomial in the size of the input and 1/ε, when either the size of the game or the support of D is constant. Finally, for the special case of the maximin objective, where the learner wishes to maximize their minimum payoff over all possible optimizer types, we construct a learner algorithm that runs in polynomial time in each step and guarantees convergence to the optimal learner payoff. All of these results make use of recently developed machinery that converts the analysis of learning algorithms to the study of the class of corresponding geometric objects known as menus.},
|
||||||
|
language = {en},
|
||||||
|
urldate = {2025-12-27},
|
||||||
|
publisher = {arXiv},
|
||||||
|
author = {Arunachaleswaran, Eshwar Ram and Collina, Natalie and Schneider, Jon},
|
||||||
|
month = feb,
|
||||||
|
year = {2025},
|
||||||
|
note = {arXiv:2412.18297 [cs]},
|
||||||
|
keywords = {Computer Science - Machine Learning, Computer Science - Computer Science and Game Theory},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/M6V9LLCS/Arunachaleswaran et al. - 2025 - Learning to Play Against Unknown Opponents.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{li_distributionally_2025,
|
||||||
|
title = {Distributionally {Robust} {Optimization} with {Adversarial} {Data} {Contamination}},
|
||||||
|
url = {http://arxiv.org/abs/2507.10718},
|
||||||
|
doi = {10.48550/arXiv.2507.10718},
|
||||||
|
abstract = {Distributionally Robust Optimization (DRO) provides a framework for decision-making under distributional uncertainty, yet its effectiveness can be compromised by outliers in the training data. This paper introduces a principled approach to simultaneously address both challenges. We focus on optimizing Wasserstein-1 DRO objectives for generalized linear models with convex Lipschitz loss functions, where an \$ε\$-fraction of the training data is adversarially corrupted. Our primary contribution lies in a novel modeling framework that integrates robustness against training data contamination with robustness against distributional shifts, alongside an efficient algorithm inspired by robust statistics to solve the resulting optimization problem. We prove that our method achieves an estimation error of \$O({\textbackslash}sqrtε)\$ for the true DRO objective value using only the contaminated data under the bounded covariance assumption. This work establishes the first rigorous guarantees, supported by efficient computation, for learning under the dual challenges of data contamination and distributional shifts.},
|
||||||
|
language = {en},
|
||||||
|
urldate = {2025-12-27},
|
||||||
|
publisher = {arXiv},
|
||||||
|
author = {Li, Shuyao and Diakonikolas, Ilias and Diakonikolas, Jelena},
|
||||||
|
month = nov,
|
||||||
|
year = {2025},
|
||||||
|
note = {arXiv:2507.10718 [cs]},
|
||||||
|
keywords = {Computer Science - Machine Learning, Mathematics - Optimization and Control, Computer Science - Data Structures and Algorithms},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/H6AXDTLX/Li et al. - 2025 - Distributionally Robust Optimization with Adversarial Data Contamination.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{karten_llm_2025,
|
||||||
|
title = {{LLM} {Economist}: {Large} {Population} {Models} and {Mechanism} {Design} in {Multi}-{Agent} {Generative} {Simulacra}},
|
||||||
|
shorttitle = {{LLM} {Economist}},
|
||||||
|
url = {http://arxiv.org/abs/2507.15815},
|
||||||
|
doi = {10.48550/arXiv.2507.15815},
|
||||||
|
abstract = {We present the LLM Economist, a novel framework that uses agent-based modeling to design and assess economic policies in strategic environments with hierarchical decision-making. At the lower level, bounded rational worker agents—instantiated as persona-conditioned prompts sampled from U.S. Census-calibrated income and demographic statistics—choose labor supply to maximize text-based utility functions learned in-context. At the upper level, a planner agent employs in-context reinforcement learning to propose piecewise-linear marginal tax schedules anchored to the current U.S. federal brackets. This construction endows economic simulacra with three capabilities requisite for credible fiscal experimentation: (i) optimization of heterogeneous utilities, (ii) principled generation of large, demographically realistic agent populations, and (iii) mechanism design—the ultimate nudging problem—expressed entirely in natural language. Experiments with populations of up to one hundred interacting agents show that the planner converges near Stackelberg equilibria that improve aggregate social welfare relative to Saez solutions, while a periodic, persona-level voting procedure furthers these gains under decentralized governance. These results demonstrate that large language model-based agents can jointly model, simulate, and govern complex economic systems, providing a tractable test bed for policy evaluation at the societal scale to help build better civilizations.},
|
||||||
|
language = {en},
|
||||||
|
urldate = {2025-12-27},
|
||||||
|
publisher = {arXiv},
|
||||||
|
author = {Karten, Seth and Li, Wenzhe and Ding, Zihan and Kleiner, Samuel and Bai, Yu and Jin, Chi},
|
||||||
|
month = jul,
|
||||||
|
year = {2025},
|
||||||
|
note = {arXiv:2507.15815 [cs]},
|
||||||
|
keywords = {Computer Science - Machine Learning, Computer Science - Multiagent Systems},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/U7A5Q78V/Karten et al. - 2025 - LLM Economist Large Population Models and Mechanism Design in Multi-Agent Generative Simulacra.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@techreport{mullapudi_reinforcement_2025,
|
||||||
|
title = {A {Reinforcement} {Learning} {Approach} to {Dynamic} {Pricing}},
|
||||||
|
abstract = {Dynamic pricing represents a critical strategic challenge in modern e-commerce, where firms must navigate fluctuating demand, inventory constraints, and aggressive competitor actions. Traditional static and heuristic-based pricing models often fail to capture the complex, non-linear dynamics of competitive digital markets, leading to suboptimal profitability. This paper proposes a model-free reinforcement learning (RL) framework to address this challenge. Specifically, we design, implement, and evaluate a Q-learning agent capable of learning an optimal, state-dependent pricing policy. The agent is trained and evaluated within a simulated market environment constructed from the publicly available "Retail Price Optimization" dataset from Kaggle, which provides a rich feature set including historical sales, product characteristics, seasonality, and, crucially, competitor pricing data. The problem is formulated as a Markov Decision Process (MDP), where the agent's state incorporates its price position relative to competitors, competitor price trends, and seasonal factors. The agent's performance is benchmarked against three baseline strategies: static pricing, a reactive "follow-the-leader" heuristic, and random pricing. The results demonstrate that the Q-learning agent achieves a substantial increase in total cumulative profit over the evaluation period, outperforming all baselines by learning a nuanced policy that strategically balances price adjustments in response to market conditions. This work provides a practical and reproducible blueprint for applying reinforcement learning to optimize pricing decisions in a simulated yet realistic competitive retail environment, highlighting the potential of RL to automate complex strategic decision-making.},
|
||||||
|
author = {Mullapudi, Pavan},
|
||||||
|
year = {2025},
|
||||||
|
note = {Publication Title: International Journal on Science and Technology (IJSAT) IJSAT25049558
|
||||||
|
Volume: 16
|
||||||
|
Issue: 4},
|
||||||
|
keywords = {Index Terms: Dynamic Pricing, Markov Decision Process, Price Optimization, Q-Learning, Reinforcement Learning, Retail Analytics},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/G95TBLF7/9558.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@techreport{roughgarden_cs364a_2013,
|
||||||
|
title = {{CS364A}: {Algorithmic} {Game} {Theory} {Lecture} \#5: {Revenue}-{Maximizing} {Auctions} *},
|
||||||
|
author = {Roughgarden, Tim},
|
||||||
|
year = {2013},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/C39VM7N9/l5.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@techreport{kuhn_distributionally_2025,
|
||||||
|
title = {Distributionally {Robust} {Optimization}},
|
||||||
|
abstract = {Distributionally robust optimization (DRO) studies decision problems under uncertainty where the probability distribution governing the uncertain problem parameters is itself uncertain. A key component of any DRO model is its ambiguity set, that is, a family of probability distributions consistent with any available structural or statistical information. DRO seeks decisions that perform best under the worst distribution in the ambiguity set. This worst case criterion is supported by findings in psychology and neuroscience, which indicate that many decision-makers have a low tolerance for distributional ambiguity. DRO is rooted in statistics, operations research and control theory, and recent research has uncovered its deep connections to regularization techniques and adversarial training in machine learning. This survey presents the key findings of the field in a unified and self-contained manner.},
|
||||||
|
author = {Kuhn, Daniel and Shafiee, Soroosh and Wiesemann, Wolfram},
|
||||||
|
year = {2025},
|
||||||
|
note = {arXiv: 2411.02549v3},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/IXTTMD7G/full-text.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{parkes_economic_2015,
|
||||||
|
title = {Economic reasoning and artificial intelligence},
|
||||||
|
volume = {349},
|
||||||
|
issn = {10959203},
|
||||||
|
doi = {10.1126/science.aaa8403},
|
||||||
|
abstract = {The field of artificial intelligence (AI) strives to build rational agents capable of perceiving the world around them and taking actions to advance specified goals. Put another way, AI researchers aim to construct a synthetic homo economicus, the mythical perfectly rational agent of neoclassical economics.We review progress toward creating this new species of machine, machina economicus, and discuss some challenges in designing AIs that can reason effectively in economic contexts. Supposing that AI succeeds in this quest, or at least comes close enough that it is useful to think about AIs in rationalistic terms, we ask how to design the rules of interaction in multi-agent systems that come to represent an economy of AIs.Theories of normative design from economics may prove more relevant for artificial agents than human agents, with AIs that better respect idealized assumptions of rationality than people, interacting through novel rules and incentive systems quite distinct from those tailored for people.},
|
||||||
|
number = {6245},
|
||||||
|
journal = {Science},
|
||||||
|
author = {Parkes, David C. and Wellman, Michael P.},
|
||||||
|
month = jul,
|
||||||
|
year = {2015},
|
||||||
|
pmid = {26185245},
|
||||||
|
note = {Publisher: American Association for the Advancement of Science},
|
||||||
|
pages = {267--272},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/27KLNFRU/_aiEcon.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{yokoo_effect_2004,
|
||||||
|
title = {The effect of false-name bids in combinatorial auctions: {New} fraud in internet auctions},
|
||||||
|
volume = {46},
|
||||||
|
issn = {08998256},
|
||||||
|
doi = {10.1016/S0899-8256(03)00045-9},
|
||||||
|
abstract = {We examine the effect of false-name bids on combinatorial auction protocols. False-name bids are bids submitted by a single bidder using multiple identifiers such as multiple e-mail addresses. The obtained results are summarized as follows: (1) the Vickrey-Clarke-Groves (VCG) mechanism, which is strategy-proof and Pareto efficient when there exists no false-name bid, is not false-name-proof; (2) there exists no false-name-proof combinatorial auction protocol that satisfies Pareto efficiency; (3) one sufficient condition where the VCG mechanism is false-name-proof is identified, i.e., the concavity of a surplus function over bidders. © 2003 Elsevier Inc. All rights reserved.},
|
||||||
|
number = {1},
|
||||||
|
journal = {Games and Economic Behavior},
|
||||||
|
author = {Yokoo, Makoto and Sakurai, Yuko and Matsubara, Shigeo},
|
||||||
|
year = {2004},
|
||||||
|
note = {Publisher: Academic Press Inc.},
|
||||||
|
keywords = {Auction, Mechanism design, Strategy-proof},
|
||||||
|
pages = {174--188},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/LUVQV6WT/Yokoo04.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@inproceedings{feldman_free-riding_2004,
|
||||||
|
title = {Free-riding and whitewashing in peer-to-peer systems},
|
||||||
|
isbn = {1-58113-942-X},
|
||||||
|
doi = {10.1145/1016527.1016539},
|
||||||
|
abstract = {We develop a model to study the phenomenon of free-riding in peer-to-peer (P2P) systems. At the heart of our model is a user of a certain type, an intrinsic and private parameter that reflects the user's willingness to contribute resources to the system. A user decides whether to contribute or free-ride based on how the current contribution cost in the system compares to her type. When the societal generosity (i.e., the average type) is low, intervention is required in order to sustain the system. We present the effect of mechanisms that exclude low type users or, more realistic, penalize free-riders with degraded service. We also consider dynamic scenarios with arrivals and departures of users, and with whitewashers: users who leave the system and rejoin with new identities to avoid reputational penalties. We find that when penalty is imposed on all newcomers in order to avoid whitewashing, system performance degrades significantly only when the turnover rate among users is high.},
|
||||||
|
booktitle = {Proceedings of the {ACM} {SIGCOMM} 2004 {Workshops}},
|
||||||
|
publisher = {Association for Computing Machinery},
|
||||||
|
author = {Feldman, Michal and Papadimitriou, Christos and Chuang, John and Stoica, Ion},
|
||||||
|
year = {2004},
|
||||||
|
keywords = {Cheap pseudonyms, Cooperation, Equilibrium, Exclusion, Free-riding, Identity cost, Incentives, Peer-to-peer, Whitewashing},
|
||||||
|
pages = {228--235},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/K32WH6SB/1016527.1016539.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{calvano_artificial_2018,
|
||||||
|
title = {Artificial {Intelligence}, {Algorithmic} {Pricing} and {Collusion}},
|
||||||
|
url = {https://papers.ssrn.com/sol3/papers.cfm?abstract_id=3304991},
|
||||||
|
doi = {10.2139/ssrn.3304991},
|
||||||
|
journal = {SSRN Electronic Journal},
|
||||||
|
author = {Calvano, Emilio and Calzolari, Giacomo and Denicolo, Vincenzo and Pastorello, Sergio},
|
||||||
|
year = {2018},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/WYTSSZBR/ssrn-3304991.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@techreport{varian_economic_1995,
|
||||||
|
title = {Economic {Mechanism} {Design} for {Computerized} {Agents}},
|
||||||
|
abstract = {The eeld of economic mechanism design has been an active area of research in economics for at least 20 years. This eld uses the tools of economics and game theory to design {\textbackslash}rules of interaction" for economic transactions that will, in principle , yield some desired outcome. In this paper I provide an overview of this subject for an audience interested in applications to electronic commerce and discuss some special problems that arise in this context.},
|
||||||
|
author = {Varian, Hal R},
|
||||||
|
year = {1995},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/S8635QX6/varian95a.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@book{russell_artificial_2021,
|
||||||
|
title = {Artificial {Intelligence} {A} {Modern} {Approach} {Fourth} {Edition} {Global} {Edition}},
|
||||||
|
isbn = {978-1-292-40117-1},
|
||||||
|
author = {Russell, Stuart and Norvig, Peter},
|
||||||
|
year = {2021},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/6B8W8S27/efdd4d1d4c2087fe1cbe03d9ced67f34.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@techreport{wellman_price_2004,
|
||||||
|
title = {Price {Prediction} in a {Trading} {Agent} {Competition} {Yevgeniy} {Vorobeychik}},
|
||||||
|
abstract = {The 2002 Trading Agent Competition (TAC) presented a challenging market game in the domain of travel shopping. One of the pivotal issues in this domain is uncertainty about hotel prices, which have a significant influence on the relative cost of alternative trip schedules. Thus, virtually all participants employ some method for predicting hotel prices. We survey approaches employed in the tournament, finding that agents apply an interesting diversity of techniques, taking into account differing sources of evidence bearing on prices. Based on data provided by entrants on their agents' actual predictions in the TAC-02 finals and semifinals, we analyze the relative efficacy of these approaches. The results show that taking into account game-specific information about flight prices is a major distinguishing factor. Machine learning methods effectively induce the relationship between flight and hotel prices from game data, and a purely analytical approach based on competitive equilibrium analysis achieves equal accuracy with no historical data. Employing a new measure of prediction quality, we relate absolute accuracy to bottom-line performance in the game.},
|
||||||
|
author = {Wellman, Michael P and Reeves, Daniel M and Lochner, Kevin M and Edu, Yvorobey@umich},
|
||||||
|
year = {2004},
|
||||||
|
note = {Publication Title: Journal of Artificial Intelligence Research
|
||||||
|
Volume: 21},
|
||||||
|
pages = {19--36},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/N9JNXFJW/live-1333-2265-jair.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@techreport{shoham_multiagent_2009,
|
||||||
|
title = {Multiagent {Systems}: {Algorithmic}, {Game}-{Theoretic}, and {Logical} {Foundations}},
|
||||||
|
url = {http://www.masfoundations.org.},
|
||||||
|
author = {Shoham, Yoav and Leyton-Brown, Kevin},
|
||||||
|
year = {2009},
|
||||||
|
keywords = {algorithms, auctions, communication, competition, cooperation, distributed problem solving, game theory, learning, logic, mechanism design, social choice},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/QZVYS7V9/shoham09a.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{xia_evaluation-driven_2025,
|
||||||
|
title = {Evaluation-{Driven} {Development} and {Operations} of {LLM} {Agents}: {A} {Process} {Model} and {Reference} {Architecture}},
|
||||||
|
url = {http://arxiv.org/abs/2411.13768},
|
||||||
|
abstract = {Large Language Models (LLMs) have enabled the emergence of LLM agents, systems capable of pursuing under-specified goals and adapting after deployment. Evaluating such agents is challenging because their behavior is open ended, probabilistic, and shaped by system-level interactions over time. Traditional evaluation methods, built around fixed benchmarks and static test suites, fail to capture emergent behaviors or support continuous adaptation across the lifecycle. To ground a more systematic approach, we conduct a multivocal literature review (MLR) synthesizing academic and industrial evaluation practices. The findings directly inform two empirically derived artifacts: a process model and a reference architecture that embed evaluation as a continuous, governing function rather than a terminal checkpoint. Together they constitute the evaluation-driven development and operations (EDDOps) approach, which unifies offline (development-time) and online (runtime) evaluation within a closed feedback loop. By making evaluation evidence drive both runtime adaptation and governed redevelopment, EDDOps supports safer, more traceable evolution of LLM agents aligned with changing objectives, user needs, and governance constraints.},
|
||||||
|
author = {Xia, Boming and Lu, Qinghua and Zhu, Liming and Xing, Zhenchang and Zhao, Dehai and Zhang, Hao},
|
||||||
|
month = nov,
|
||||||
|
year = {2025},
|
||||||
|
note = {arXiv: 2411.13768},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/H8IS64AW/2411.13768v2.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@techreport{xie_osworld_2024,
|
||||||
|
title = {{OSWORLD}: {Benchmarking} {Multimodal} {Agents} for {Open}-{Ended} {Tasks} in {Real} {Computer} {Environments}},
|
||||||
|
url = {https://os-world.github.io},
|
||||||
|
abstract = {Autonomous agents that accomplish complex computer tasks with minimal human interventions have the potential to transform human-computer interaction, significantly enhancing accessibility and productivity. However, existing benchmarks either lack an interactive environment or are limited to environments specific to certain applications or domains, failing to reflect the diverse and complex nature of real-world computer use, thereby limiting the scope of tasks and agent scalability. To address this issue, we introduce OSWORLD, the first-of-its-kind scalable, real computer environment for multimodal agents, supporting task setup, execution-based evaluation, and interactive learning across various operating systems such as Ubuntu, Windows, and macOS. OSWORLD can serve as a unified, integrated computer environment for assessing open-ended computer tasks that involve arbitrary applications. Building upon OSWORLD, we create a benchmark of 369 computer tasks involving real web and desktop apps in open domains, OS file I/O, and workflows spanning multiple applications. Each task example is derived from real-world computer use cases and includes a detailed initial state setup configuration and a custom execution-based evaluation script for reliable, reproducible evaluation. Extensive evaluation of state-of-the-art LLM/VLM-based agents on OSWORLD reveals significant deficiencies in their ability to serve as computer assistants. While humans can accomplish over 72.36\% of the tasks, the best model achieves only 12.24\% success, primarily struggling with GUI grounding and operational knowledge. Comprehensive analysis using OSWORLD provides valuable insights for developing multimodal generalist agents that were not possible with previous benchmarks. Our code, environment, baseline models, and data are publicly available at https://os-world.github.io.},
|
||||||
|
author = {Xie, Tianbao and Zhang, Danyang and Chen, Jixuan and Li, Xiaochuan and Zhao, Siheng and Cao, Ruisheng and Jing Hua, Toh and Cheng, Zhoujun and Shin, Dongchan and Lei, Fangyu and Liu, Yitao and Xu, Yiheng and Zhou, Shuyan and Savarese, Silvio and Xiong, Caiming and Zhong, Victor and Yu, Tao},
|
||||||
|
month = may,
|
||||||
|
year = {2024},
|
||||||
|
note = {arXiv: 2404.07972v2},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/LLRKXIC7/full-text.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@techreport{imperva_rapid_2025,
|
||||||
|
title = {The {Rapid} {Rise} of {Bots} and the {Unseen} {Risk} for {Business} \#{2025BADBOTREPORT}},
|
||||||
|
author = {{Imperva}},
|
||||||
|
year = {2025},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/AWR9IQRD/2025-Bad-Bot-Report.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{perez-ricardo_exploring_2025,
|
||||||
|
title = {Exploring booking intentions through price elasticity of demand in tourism accommodations using large-scale data analytics},
|
||||||
|
volume = {31},
|
||||||
|
issn = {24448834},
|
||||||
|
doi = {10.1016/j.iedeen.2025.100271},
|
||||||
|
abstract = {The study aims to explore tourists' booking intentions by analyzing the price elasticity of demand in tourist accommodations. This analysis should reveal how changes in price affect booking behavior across different customer segments, using online booking records. A dataset was compiled from 106 hotels in Malaga, Spain, comprising 27,910 online bookings sourced exclusively from hotel websites. To understand the price elasticity of demand, a simple log-log regression was applied, segmenting the data based on key revenue-related variables. Subsequently, a cluster segmentation was performed using the Elbow method and K-means algorithm to identify distinct market segments. The findings highlighted that Family Travelers and Short Stay Travelers segments exhibited elastic demand, indicating higher sensitivity to price fluctuations. In contrast, Early Bookers and Mid-Season Long Stayers demonstrated inelastic demand, with lower responsiveness to changes in tourist accommodation prices. The number of variables analyzed in this study, along with the cluster analysis, represent a novelty and contribute to the existing literature on market segmentation and price elasticity of demand. This integration enriches both fields of research, offering mutual benefits and deeper insights that enhance the understanding of booking intention and pricing strategies.},
|
||||||
|
number = {1},
|
||||||
|
urldate = {2025-11-28},
|
||||||
|
journal = {European Research on Management and Business Economics},
|
||||||
|
author = {Pérez-Ricardo, Elizabeth del Carmen and García-Mestanza, Josefa},
|
||||||
|
month = jan,
|
||||||
|
year = {2025},
|
||||||
|
note = {Publisher: European Academy of Management and Business Economics},
|
||||||
|
keywords = {Booking intention, Price elasticity, Tourist segmentation},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/QNXZJLRM/S2444883425000038.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{ghaffary_amazon_2025,
|
||||||
|
title = {Amazon {Sues} to {Stop} {Perplexity} {From} {Using} {AI} {Tool} to {Buy} {Stuff}},
|
||||||
|
url = {https://www.bloomberg.com/news/articles/2025-11-04/amazon-demands-perplexity-stop-ai-agent-from-making-purchases},
|
||||||
|
author = {Ghaffary, Shirin and Day, Matt},
|
||||||
|
month = nov,
|
||||||
|
year = {2025},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/IQL6FPWE/Amazon Sues to Stop Perplexity From Using AI Tool to Buy Stuff - Bloomberg.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@techreport{besbes_dynamic_2007,
|
||||||
|
title = {Dynamic {Pricing} {Without} {Knowing} the {Demand} {Function}: {Risk} {Bounds} and {Near}-{Optimal} {Algorithms} *},
|
||||||
|
abstract = {We consider a single product revenue management problem where, given an initial inventory, the objective is to dynamically adjust prices over a finite sales horizon to maximize expected revenues. Realized demand is observed over time, but the underlying functional relationship between price and mean demand rate that governs these observations (otherwise known as the demand function or demand curve), is not known. We consider two instances of this problem: i.) a setting where the demand function is assumed to belong to a known parametric family with unknown parameter values; and ii.) a setting where the demand function is assumed to belong to a broad class of functions that need not admit any parametric representation. In each case we develop policies that learn the demand function "on the fly," and optimize prices based on that. The performance of these algorithms is measured in terms of the regret: the revenue loss relative to the maximal revenues that can be extracted when the demand function is known prior to the start of the selling season. We derive lower bounds on the regret that hold for any admissible pricing policy, and then show that our proposed algorithms achieve a regret that is "close" to this lower bound. The magnitude of the regret can be interpreted as the economic value of prior knowledge on the demand function; manifested as the revenue loss due to model uncertainty.},
|
||||||
|
author = {Besbes, Omar and Zeevi, Assaf},
|
||||||
|
month = dec,
|
||||||
|
year = {2007},
|
||||||
|
note = {Publication Title: Operations Research},
|
||||||
|
keywords = {learning, asymptotic analysis, estimation, exploration-exploitation, pricing, Revenue management, value of information},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/SBAIB4V2/Dp_wo_demand_risk_ob_az_posted.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@techreport{markntel_advisors_global_2025,
|
||||||
|
address = {Noida, Uttar Pradesh, India},
|
||||||
|
title = {Global {AI} {Agent} {Market} {Research} {Report}: {Forecast} (2026–2032)},
|
||||||
|
url = {https://www.marknteladvisors.com/research-library/ai-agent-market.html},
|
||||||
|
urldate = {2025-12-12},
|
||||||
|
institution = {MarkNtel Advisors},
|
||||||
|
author = {{MarkNtel Advisors}},
|
||||||
|
year = {2025},
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{amjad_censored_2017,
|
||||||
|
title = {Censored {Demand} {Estimation} in {Retail}},
|
||||||
|
volume = {1},
|
||||||
|
url = {https://par.nsf.gov/servlets/purl/10066022},
|
||||||
|
doi = {10.1145/3154489},
|
||||||
|
abstract = {In this paper, the question of interest is estimating true demand of a product at a given store location and time period in the retail environment based on a single noisy and potentially censored observation. To address this question, we introduce a \%non-parametric framework to make inference from multiple time series. Somewhat surprisingly, we establish that the algorithm introduced for the purpose of "matrix completion" can be used to solve the relevant inference problem. Specifically, using the Universal Singular Value Thresholding (USVT) algorithm [7], we show that our estimator is consistent: the average mean squared error of the estimated average demand with respect to the true average demand goes to 0 as the number of store locations and time intervals increase to \${\textbackslash}infty\$. We establish naturally appealing properties of the resulting estimator both analytically as well as through a sequence of instructive simulations. Using a real dataset in retail (Walmart), we argue for the practical relevance of our approach.},
|
||||||
|
number = {2},
|
||||||
|
urldate = {2025-11-12},
|
||||||
|
journal = {Proceedings of the ACM on Measurement and Analysis of Computing Systems},
|
||||||
|
author = {Amjad, Muhammad J. and Shah, Devavrat},
|
||||||
|
month = dec,
|
||||||
|
year = {2017},
|
||||||
|
note = {Publisher: Association for Computing Machinery (ACM)},
|
||||||
|
pages = {1--28},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/5ZYADDT4/10066022.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{ganie_uncertainty_2025,
|
||||||
|
title = {Uncertainty in {Authorship}: {Why} {Perfect} {AI} {Detection} {Is} {Mathematically} {Impossible}},
|
||||||
|
shorttitle = {Uncertainty in {Authorship}},
|
||||||
|
url = {http://arxiv.org/abs/2509.11915},
|
||||||
|
doi = {10.48550/arXiv.2509.11915},
|
||||||
|
abstract = {As large language models (LLMs) become more advanced, it is increasingly difficult to distinguish between human-written and AI-generated text. This paper draws a conceptual parallel between quantum uncertainty and the limits of authorship detection in natural language. We argue that there is a fundamental trade-off: the more confidently one tries to identify whether a text was written by a human or an AI, the more one risks disrupting the text's natural flow and authenticity. This mirrors the tension between precision and disturbance found in quantum systems. We explore how current detection methods--such as stylometry, watermarking, and neural classifiers--face inherent limitations. Enhancing detection accuracy often leads to changes in the AI's output, making other features less reliable. In effect, the very act of trying to detect AI authorship introduces uncertainty elsewhere in the text. Our analysis shows that when AI-generated text closely mimics human writing, perfect detection becomes not just technologically difficult but theoretically impossible. We address counterarguments and discuss the broader implications for authorship, ethics, and policy. Ultimately, we suggest that the challenge of AI-text detection is not just a matter of better tools--it reflects a deeper, unavoidable tension in the nature of language itself.},
|
||||||
|
language = {en},
|
||||||
|
urldate = {2026-01-05},
|
||||||
|
publisher = {arXiv},
|
||||||
|
author = {Ganie, Aadil Gani},
|
||||||
|
month = sep,
|
||||||
|
year = {2025},
|
||||||
|
note = {arXiv:2509.11915 [cs]},
|
||||||
|
keywords = {Computer Science - Computation and Language},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/3Z2XK4QC/Ganie - 2025 - Uncertainty in Authorship Why Perfect AI Detection Is Mathematically Impossible.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{shi_distributionally_2024,
|
||||||
|
title = {Distributionally {Robust} {Model}-{Based} {Offline} {Reinforcement} {Learning} with {Near}-{Optimal} {Sample} {Complexity}},
|
||||||
|
abstract = {This paper concerns the central issues of model robustness and sample efficiency in offline reinforcement learning (RL), which aims to learn to perform decision making from history data without active exploration. Due to uncertainties and variabilities of the environment, it is critical to learn a robust policy—with as few samples as possible—that performs well even when the deployed environment deviates from the nominal one used to collect the history dataset. We consider a distributionally robust formulation of offline RL, focusing on tabular robust Markov decision processes with an uncertainty set specified by the Kullback-Leibler divergence in both finite-horizon and infinite-horizon settings. To combat with sample scarcity, a model-based algorithm that combines distributionally robust value iteration with the principle of pessimism in the face of uncertainty is proposed, by penalizing the robust value estimates with a carefully designed data-driven penalty term. Under a mild and tailored assumption of the history dataset that measures distribution shift without requiring full coverage of the state-action space, we establish the finite-sample complexity of the proposed algorithms. We further develop an informationtheoretic lower bound, which suggests that learning RMDPs is at least as hard as the standard MDPs when the uncertainty level is sufficient small, and corroborates the tightness of our upper bound up to polynomial factors of the (effective) horizon length for a range of uncertainty levels. To the best our knowledge, this provides the first provably near-optimal robust offline RL algorithm that learns under model uncertainty and partial coverage.},
|
||||||
|
language = {en},
|
||||||
|
author = {Shi, Laixi and Chi, Yuejie},
|
||||||
|
month = jun,
|
||||||
|
year = {2024},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/K56G4EIP/Shi and Chi - Distributionally Robust Model-Based Offline Reinforcement Learning with Near-Optimal Sample Complexity.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{dutting_mechanism_2025,
|
||||||
|
title = {Mechanism {Design} for {Large} {Language} {Models} ({Extended} {Abstract})},
|
||||||
|
abstract = {We investigate auction mechanisms for AIgenerated content, focusing on applications like ad creative generation. In our model, agents’ preferences over stochastically generated content are encoded as large language models (LLMs). We propose an auction format that operates on a tokenby-token basis, and allows LLM agents to influence content creation through single dimensional bids. We formulate two desirable incentive properties and prove their equivalence to a monotonicity condition on output aggregation. This equivalence enables a second-price rule design, even absent explicit agent valuation functions. Our design is supported by demonstrations on a publicly available LLM.},
|
||||||
|
language = {en},
|
||||||
|
author = {Dütting, Paul and Mirrokni, Vahab and Leme, Renato Paes and Xu, Haifeng and Zuo, Song},
|
||||||
|
year = {2025},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/2ABDEYDN/Dütting et al. - Mechanism Design for Large Language Models (Extended Abstract).pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{fcmi_machine_2025,
|
||||||
|
title = {Machine {Speed} {Markets}: {AI} {Agent} {Market} {Strategy} \& {Growth}},
|
||||||
|
shorttitle = {Machine {Speed} {Markets}},
|
||||||
|
url = {https://www.360strategy.co.uk/post/machine-speed-markets-ai-agents},
|
||||||
|
abstract = {Recent research by NBER economists suggests these AI agents in particular, could drive a "Coasean singularity," a point where transaction costs fall towards zero, radically reshaping how markets function. In essence, tasks like finding information, negotiating deals, and enforcing contracts which are traditionally costly frictions in commerce, may become nearly instantaneous and costless.},
|
||||||
|
language = {en},
|
||||||
|
urldate = {2026-01-20},
|
||||||
|
journal = {360 Strategy},
|
||||||
|
author = {FCMi, CMgr, Mark Evans MBA},
|
||||||
|
month = nov,
|
||||||
|
year = {2025},
|
||||||
|
file = {Snapshot:/home/velocitatem/Zotero/storage/Z22P9JJH/machine-speed-markets-ai-agents.html:text/html},
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{coase_nature_1937,
|
||||||
|
title = {The {Nature} of the {Firm}},
|
||||||
|
volume = {4},
|
||||||
|
issn = {1468-0335},
|
||||||
|
url = {https://onlinelibrary.wiley.com/doi/abs/10.1111/j.1468-0335.1937.tb00002.x},
|
||||||
|
doi = {10.1111/j.1468-0335.1937.tb00002.x},
|
||||||
|
language = {en},
|
||||||
|
number = {16},
|
||||||
|
urldate = {2026-01-20},
|
||||||
|
journal = {Economica},
|
||||||
|
author = {Coase, R. H.},
|
||||||
|
year = {1937},
|
||||||
|
pages = {386--405},
|
||||||
|
file = {Full Text PDF:/home/velocitatem/Zotero/storage/TABLLPEU/Coase - 1937 - The Nature of the Firm.pdf:application/pdf;Snapshot:/home/velocitatem/Zotero/storage/Q5RFW9LJ/j.1468-0335.1937.tb00002.html:text/html},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{fish_algorithmic_2025,
|
||||||
|
title = {Algorithmic {Collusion} by {Large} {Language} {Models}},
|
||||||
|
url = {http://arxiv.org/abs/2404.00806},
|
||||||
|
doi = {10.48550/arXiv.2404.00806},
|
||||||
|
abstract = {The rise of algorithmic pricing raises concerns of algorithmic collusion. We conduct experiments with algorithmic pricing agents based on Large Language Models (LLMs). We find that LLM-based pricing agents quickly and autonomously reach supracompetitive prices and profits in oligopoly settings and that variation in seemingly innocuous phrases in LLM instructions (“prompts”) may substantially influence the degree of supracompetitive pricing. Off-path analysis using novel techniques uncovers price-war concerns as contributing to these phenomena. Our results extend to auction settings. Our findings uncover unique challenges to any future regulation of LLM-based pricing agents, and AI-based pricing agents more broadly.},
|
||||||
|
language = {en},
|
||||||
|
urldate = {2026-01-20},
|
||||||
|
publisher = {arXiv},
|
||||||
|
author = {Fish, Sara and Gonczarowski, Yannai A. and Shorrer, Ran I.},
|
||||||
|
month = sep,
|
||||||
|
year = {2025},
|
||||||
|
note = {arXiv:2404.00806 [econ]},
|
||||||
|
keywords = {Computer Science - Computer Science and Game Theory, Computer Science - Artificial Intelligence, Economics - General Economics},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/QHWVISCZ/Fish et al. - 2025 - Algorithmic Collusion by Large Language Models.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{hardt_strategic_2015,
|
||||||
|
title = {Strategic {Classification}},
|
||||||
|
url = {http://arxiv.org/abs/1506.06980},
|
||||||
|
doi = {10.48550/arXiv.1506.06980},
|
||||||
|
abstract = {Machine learning relies on the assumption that unseen test instances of a classification problem follow the same distribution as observed training data. However, this principle can break down when machine learning is used to make important decisions about the welfare (employment, education, health) of strategic individuals. Knowing information about the classifier, such individuals may manipulate their attributes in order to obtain a better classification outcome. As a result of this behavior—often referred to as gaming—the performance of the classifier may deteriorate sharply. Indeed, gaming is a well-known obstacle for using machine learning methods in practice; in financial policy-making, the problem is widely known as Goodhart’s law. In this paper, we formalize the problem, and pursue algorithms for learning classifiers that are robust to gaming.},
|
||||||
|
language = {en},
|
||||||
|
urldate = {2026-01-20},
|
||||||
|
publisher = {arXiv},
|
||||||
|
author = {Hardt, Moritz and Megiddo, Nimrod and Papadimitriou, Christos and Wootters, Mary},
|
||||||
|
month = nov,
|
||||||
|
year = {2015},
|
||||||
|
note = {arXiv:1506.06980 [cs]},
|
||||||
|
keywords = {Computer Science - Machine Learning},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/HNCDYGWS/Hardt et al. - 2015 - Strategic Classification.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{liu_contextual_2024,
|
||||||
|
title = {Contextual {Dynamic} {Pricing} with {Strategic} {Buyers}},
|
||||||
|
url = {http://arxiv.org/abs/2307.04055},
|
||||||
|
doi = {10.48550/arXiv.2307.04055},
|
||||||
|
abstract = {Personalized pricing, which involves tailoring prices based on individual characteristics, is commonly used by firms to implement a consumer-specific pricing policy. In this process, buyers can also strategically manipulate their feature data to obtain a lower price, incurring certain manipulation costs. Such strategic behavior can hinder firms from maximizing their profits. In this paper, we study the contextual dynamic pricing problem with strategic buyers. The seller does not observe the buyer's true feature, but a manipulated feature according to buyers' strategic behavior. In addition, the seller does not observe the buyers' valuation of the product, but only a binary response indicating whether a sale happens or not. Recognizing these challenges, we propose a strategic dynamic pricing policy that incorporates the buyers' strategic behavior into the online learning to maximize the seller's cumulative revenue. We first prove that existing non-strategic pricing policies that neglect the buyers' strategic behavior result in a linear \$Ω(T)\$ regret with \$T\$ the total time horizon, indicating that these policies are not better than a random pricing policy. We then establish that our proposed policy achieves a sublinear regret upper bound of \$O({\textbackslash}sqrt\{T\})\$. Importantly, our policy is not a mere amalgamation of existing dynamic pricing policies and strategic behavior handling algorithms. Our policy can also accommodate the scenario when the marginal cost of manipulation is unknown in advance. To account for it, we simultaneously estimate the valuation parameter and the cost parameter in the online pricing policy, which is shown to also achieve an \$O({\textbackslash}sqrt\{T\})\$ regret bound. Extensive experiments support our theoretical developments and demonstrate the superior performance of our policy compared to other pricing policies that are unaware of the strategic behaviors.},
|
||||||
|
language = {en},
|
||||||
|
urldate = {2026-01-20},
|
||||||
|
publisher = {arXiv},
|
||||||
|
author = {Liu, Pangpang and Yang, Zhuoran and Wang, Zhaoran and Sun, Will Wei},
|
||||||
|
month = jun,
|
||||||
|
year = {2024},
|
||||||
|
note = {arXiv:2307.04055 [stat]},
|
||||||
|
keywords = {Computer Science - Machine Learning, Statistics - Machine Learning, Computer Science - Computer Science and Game Theory, Computer Science - Artificial Intelligence},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/MVJNULK3/Liu et al. - 2024 - Contextual Dynamic Pricing with Strategic Buyers.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@techreport{dhir_http_2025,
|
||||||
|
type = {Internet {Draft}},
|
||||||
|
title = {{HTTP} {Agent} {Profile} ({HAP}): {Authenticated} and {Monetized} {Agent} {Traffic} on the {Web}},
|
||||||
|
shorttitle = {{HTTP} {Agent} {Profile} ({HAP})},
|
||||||
|
url = {https://datatracker.ietf.org/doc/draft-dhir-http-agent-profile},
|
||||||
|
abstract = {Autonomous agents such as LLM-powered crawlers, browser-integrated assistants, and task-oriented bots are rapidly becoming first-class HTTP clients on the Web. Today’s infrastructure largely assumes a human behind a browser and monetizes content through advertising and coarse subscriptions. Automated agents consume content at scale without rendering pages or viewing ads, exacerbating bot-mitigation arms races and economic misalignment between content providers and AI systems. This document describes an HTTP Agent Profile (HAP) that enables: (1) cryptographic authentication of agent traffic using HTTP Message Signatures; (2) clear separation between human and agent traffic using privacy-preserving human tokens; and (3) protocol-level value exchange for agents via HTTP status code 402 ("Payment Required") and pluggable micropayment mechanisms. The profile reuses existing HTTP features and is designed for incremental deployment via reverse proxies, CDNs, and agent libraries.},
|
||||||
|
number = {draft-dhir-http-agent-profile-00},
|
||||||
|
urldate = {2026-01-20},
|
||||||
|
institution = {Internet Engineering Task Force},
|
||||||
|
author = {Dhir, Sanat},
|
||||||
|
month = nov,
|
||||||
|
year = {2025},
|
||||||
|
note = {Num Pages: 13},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{noauthor_amazoncom_2026,
|
||||||
|
title = {Amazon.com {Services} {LLC} v. {Perplexity} {AI}, {Inc}},
|
||||||
|
language = {en},
|
||||||
|
month = jan,
|
||||||
|
year = {2026},
|
||||||
|
note = {No. 3:25-cv-09514-MMC},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/4JWZSTXJ/Posner - UNITED STATES DISTRICT COURT NORTHERN DISTRICT OF CALIFORNIA SAN FRANCISCO DIVISION.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@article{wright_2026_2025,
|
||||||
|
title = {2026 {Artificial} {Intelligence} {Outlook}: {The} {Great} {Competition} {Wars} {Have} {Begun}},
|
||||||
|
language = {en},
|
||||||
|
journal = {Pitchbook},
|
||||||
|
author = {Wright, Brian and Javaheri, Ali and Bellomo, Eric and Hernandez, Derek and Yang, Rudy and MacDonagh, John and DeGagne, Aaron and Frederick, Alex and Geurkink, Jonathan and Zabelin, Dimitri and Ulan, James},
|
||||||
|
month = dec,
|
||||||
|
year = {2025},
|
||||||
|
file = {PDF:/home/velocitatem/Zotero/storage/AIY5K3TX/Wright et al. - 2025 - Institutional Research Group.pdf:application/pdf},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{rachitsky_marc_2026,
|
||||||
|
title = {Marc {Andreessen}: {The} real {AI} boom hasn’t even started yet},
|
||||||
|
shorttitle = {Marc {Andreessen}},
|
||||||
|
url = {https://www.lennysnewsletter.com/p/marc-andreessen-the-real-ai-boom},
|
||||||
|
abstract = {On raising kids, why job loss fears are overblown, the future of PM/eng/design careers, and the macro force you should pay attention to},
|
||||||
|
language = {en},
|
||||||
|
urldate = {2026-02-01},
|
||||||
|
author = {Rachitsky, Lenny},
|
||||||
|
month = feb,
|
||||||
|
year = {2026},
|
||||||
|
file = {Snapshot:/home/velocitatem/Zotero/storage/DGW8PHMV/marc-andreessen-the-real-ai-boom.html:text/html},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{noauthor_tpu_2025,
|
||||||
|
title = {{TPU} v6e},
|
||||||
|
url = {https://cloud.google.com/tpu/docs/v6e},
|
||||||
|
language = {es-419-x-mtfrom-en},
|
||||||
|
urldate = {2026-02-17},
|
||||||
|
journal = {Google Cloud Documentation},
|
||||||
|
month = dec,
|
||||||
|
year = {2025},
|
||||||
|
file = {Snapshot:/home/velocitatem/Zotero/storage/RNMB32KD/v6e.html:text/html},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{noauthor_tpu_2025-1,
|
||||||
|
title = {{TPU} v5e {\textbar} {Google} {Cloud} {Documentation}},
|
||||||
|
url = {https://cloud.google.com/tpu/docs/v5e},
|
||||||
|
language = {es-419-x-mtfrom-en},
|
||||||
|
urldate = {2026-02-17},
|
||||||
|
month = dec,
|
||||||
|
year = {2025},
|
||||||
|
file = {Snapshot:/home/velocitatem/Zotero/storage/BLLG9NZC/v5e.html:text/html},
|
||||||
|
}
|
||||||
|
|
||||||
|
@misc{noauthor_tpu_2026,
|
||||||
|
title = {{TPU} v4 {\textbar} {Google} {Cloud} {Documentation}},
|
||||||
|
url = {https://cloud.google.com/tpu/docs/v4},
|
||||||
|
language = {es-419-x-mtfrom-en},
|
||||||
|
urldate = {2026-02-17},
|
||||||
|
month = feb,
|
||||||
|
year = {2026},
|
||||||
|
file = {Snapshot:/home/velocitatem/Zotero/storage/N724QGF6/v4.html:text/html},
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,9 +8,60 @@
|
|||||||
|
|
||||||
\section{Introduction}
|
\section{Introduction}
|
||||||
|
|
||||||
Research Objectives and Contribution: What are we making, why and who should care?
|
In this paper we present an exploration and defense against the presence of new commercial entities in digitally powered platforms, preserving market equilibrium in the age of AI. This research establishes the following contributions: definition and formalization of non-human transactors in e-commerce platforms, development of a testing-ground for capturing the behavioral essence of these transactors across a large variety of digital systems, construction of a discriminative model (to prove separability) as a strong learner for downstream mitigation of contamination by non-human entities, translation of such learned separability into existing dynamic pricing machine learning loops, and finally establishment of a high-level KPI-affecting causal effect and cost-saving framework for the future of internet commerce in the presence of such non-human learners.
|
||||||
|
|
||||||
|
This research effort touches a large variety of domains, spanning behavioral economics for understanding the rationality of behavior as theorized by the concept of homo economicus, agent-based modeling to translate our learned separability into disjoint dynamic pricing systems, reinforcement learning which serves as the SOTA for price-learners, and dynamic pricing and market equilibrium theory to understand the risks of possible supra-competitive pricing phenomena in cases of adversarial pricing systems driving the market out of equilibrium. \footnote{Given the rapid evolution of the field we acknowledge all developments with a cutoff set at the date of March 31st 2026.}
|
||||||
|
|
||||||
\subsection{Motivation and Market Context}
|
\subsection{Motivation and Market Context}
|
||||||
Current market dynamics and trends of dynamic pricing and AI agents. Future projections of AI agents. Key stakeholders that are discussing this and reporting on it (Thales). Who is most affected
|
|
||||||
|
The current innovation boom in generative artificial intelligence and its applications to knowledge-based work tasks has brought many competing technologies for browser-use automation, with benchmarks and evaluations \parencite{xia_evaluation-driven_2025} motivating the development of capabilities focused on commercial research, understanding, and transaction execution \parencite{xie_osworld_2024}. The ``AI Agent'' market is forecasted to grow from around USD 5-8 billion in 2025 to USD 42-52 billion by 2030. This surge reflects adoption in e-commerce, customer service, and enterprise automation, where agents handle interactions previously done by humans, raising the question of how these systems should be designed for future robustness as well as how to maintain a competitive edge in the analytical components of e-commerce platforms \parencite{markntel_advisors_global_2025}.
|
||||||
|
|
||||||
|
The key stakeholders affected by the threat of increasing agent-driven traffic include online businesses and platform operators (especially in bot-heavy sectors like retail, travel, and financial services), their security, fraud, and engineering teams, end users whose accounts and data are exposed and whose experience degrades, regulators and legal stakeholders responding to breaches and fraud, and the attackers or bot operators driving the automation \parencite{imperva_rapid_2025}.
|
||||||
|
|
||||||
|
The industry has already seen legal action in cases like Amazon against Perplexity \parencite{ghaffary_amazon_2025}, stemming from the difficulty of identifying traffic from hybrid systems like the Commet browser. This paper explores such systems to better understand what the interaction data looks like and what it means for dynamic pricing and recommendation systems downstream. This observed impact indicates a need for prevention of secondary negative effects on the ``legacy'' systems which power modern revenue sources for many companies. Dynamic pricing algorithms rely on directly translating demand features $q$ to new price assignments $\hat{p}$ across a catalogue of products of size $N$. This opens opportunities to design a \textit{tabula rasa} of digital market mechanisms that will shape the future of commerce in the age of artificial intelligence.
|
||||||
|
|
||||||
\subsection{Solution Space Overview}
|
\subsection{Solution Space Overview}
|
||||||
Different approaches and perspectives, here also add a preview of what will be developed and explored in the lit review.
|
Dynamic pricing systems, as presented by \textcite{mueller_low-rank_2019}, often deal with sparse low-rank data of demand signals which, combined with contamination from agents, creates complex interactions that impact pricing. To further complicate the problem, certain commercial settings such as the one presented by \textcite{amjad_censored_2017} must address the true demand of products under censored observations. This provides a formulation for handling demand in our case with multiple kinds of commercial mediators: $\hat{q} \gets q_A + q_H$ where $q_A$ represents the distribution of demand generated by agentic mediators and $q_H$ represents that of true human demand, these are two distinct populations with divergent objective functions.
|
||||||
|
|
||||||
|
We formally define interaction data as coming from some actor which can either be an agent ($A$) or human ($H$). For purposes of this research, an agent is an algorithmic loop with the ability to access a web platform and perform actions such as clicks, scrolls, and input field fills. The loop terminates when the internal large language model judges the provided task definition as complete. A detailed breakdown can be found in \cref{algagent-loop}.
|
||||||
|
|
||||||
|
\subsection{Research Questions}
|
||||||
|
|
||||||
|
This dissertation is organized around one main research question and three supporting sub-questions:
|
||||||
|
\begin{enumerate}
|
||||||
|
\item[\textbf{Main RQ}] How can dynamic pricing systems preserve margin integrity when transaction orchestration is increasingly mediated by non-human agents?
|
||||||
|
\item[\textbf{SQ1}] \textit{Separability}: Can agent and human sessions be reliably distinguished from behavioral interaction signals alone, without relying on network-level or device fingerprinting?
|
||||||
|
\item[\textbf{SQ2}] \textit{Theoretical Impact}: What is the formal relationship between agent contamination levels and the erosion of pricing power in dynamic pricing systems?
|
||||||
|
\item[\textbf{SQ3}] \textit{Robust Mitigation}: How can pricing policies be constructed to maintain margin integrity under unknown and non-stationary levels of agent contamination?
|
||||||
|
\end{enumerate}
|
||||||
|
|
||||||
|
|
||||||
|
\begin{algorithm}[t]
|
||||||
|
\DontPrintSemicolon
|
||||||
|
|
||||||
|
\SetKwInput{Input}{Input}
|
||||||
|
\SetKwInput{Output}{Output}
|
||||||
|
|
||||||
|
\Input{Goal $G$, Platform URL $u$, LLM $\mathcal{M}$}
|
||||||
|
\Output{Task completion result $r$}
|
||||||
|
|
||||||
|
Initialize browser instance $\mathcal{B}$ with connection to $u$\;
|
||||||
|
Construct prompt $\pi \gets \textsc{BuildPrompt}(G, u)$\;
|
||||||
|
$\text{done} \gets \text{False}$\;
|
||||||
|
|
||||||
|
\While{$\neg \text{done}$}{
|
||||||
|
Observe current page state $s_t$ from $\mathcal{B}$\;
|
||||||
|
Query $\mathcal{M}$ with $(\pi, s_t)$ to determine next action $a_t \in \{\text{click}, \text{scroll}, \text{fill}, \text{navigate}\}$\;
|
||||||
|
Execute $a_t$ on $\mathcal{B}$ to transition to state $s_{t+1}$\;
|
||||||
|
$\text{done} \gets \mathcal{M}.\textsc{JudgeCompletion}(G, s_{t+1})$\;
|
||||||
|
}
|
||||||
|
|
||||||
|
Extract final result $r$ from terminal state\;
|
||||||
|
\Return{$r$}\;
|
||||||
|
|
||||||
|
\caption{AI Agent's Interaction Loop}
|
||||||
|
\label{algagent-loop}
|
||||||
|
\end{algorithm}
|
||||||
|
|
||||||
|
|
||||||
|
The previously described goal of separability allows us to formulate a task which entails taking raw interaction data for either actor and creating a composite demand estimate $\hat{q}$. We propose a robust optimization objective defined in our methodology, transforming the pricing problem into a form of Distributionally Robust Optimization \parencite{kuhn_distributionally_2025} where the learner must guard against adversarial contamination in observed demand distributors. In this setting we must learn to make decision that perform under the assumption of not having a single estimated probability distribution but under an ambiguity set of any distribution, of which we have limited information. In our case as stated is a mixture of distributions with a parameter which is unknown and non-stationary.
|
||||||
|
|||||||
@@ -1,17 +1,72 @@
|
|||||||
\section{Literature Review}
|
\section{Literature Review}
|
||||||
|
|
||||||
\subsection{Foundational Concepts}
|
To better understand all wedges of the current works, we must start by exploring the nature of agents, agentic computer use and web automation, complementing that with economic reasoning and strategic interaction. The final surface to cover, leads us to data-driven dynamic pricing under uncertainty. The key technical risk is not ``agents buying things'' per se, but agents shaping the behavioral and demand signals that downstream pricing systems consume and depend on. This latter case of agents shopping is currently pending legal action in the case of \textcite{noauthor_amazoncom_2026} which is currently being treated as a violation of the Computer Fraud and Abuse Act. The introduction of these mediating actor entities into economic systems, is further creating a threat of false-name bidding \parencite{yokoo_effect_2004}, which prior research has explored in a trading context. Other research on pseudonyms in dynamic systems, demonstrate whitewashing in AI agents which can ignore defensive mechanisms by re-entry with different identities \parencite{feldman_free-riding_2004}. Dynamic pricing assumes demand proxies are behaviorally meaningful, while bot detection aims at security and access control. The missing bridge is a principled framework for separating non-human reconnaissance from genuine human demand expression and integrating that separation into pricing heuristics without degrading legitimate user experience (in our research tracked by the user-experience index). This gap, is what our contribution aims to address, particularly for the aforementioned stakeholder groups.
|
||||||
|
|
||||||
What is the taxonomy and definition of an agent and an actor in this case, a bit more about interaction models in sessions and about dynamic pricing algorithms.
|
\subsection{Agent Taxonomy and Definitions}
|
||||||
|
|
||||||
|
An agent in the context of artificial intelligence is generally defined by anything that can reason and act upon observations of its environments (collected through some sensory inputs) and carry out actions through effectors. Moreover, a rational agent is an entity that is capable of perceiving the world around them and taking actions to advance specified goals. This definition by \textcite{russell_artificial_2021} is further developed in an economic context by \textcite{parkes_economic_2015}, suggesting AI research attempts to construct a synthetic \textit{homo economicus}, which may also be termed \textit{machina economicus}.
|
||||||
|
A specific class or taxon of this \textit{machina economicus}, the Large Language Model (LLM) agent, is defined as an autonomous system capable of achieving goals and adapting post-training, often without needing explicit code or fundamental model changes \parencite{xia_evaluation-driven_2025}.
|
||||||
|
|
||||||
|
We must however acknowledge the current SOTA as presented by OSWORLD simulations by \textcite{xie_osworld_2024} have demonstrated that multi-modal tasks across desktop and web interaction modes, have a top-performing score of only 12.24\% success, whereas humans have a higher 72\% success rate; this is linked to the lack of grounding of these agents and their inability of handling unexpected errors. This weakness matters for this research because it clarifies the near-term threat model: practical exploitation does not require a fully competent ``computer assistant'', only enough automation to perform high-volume reconnaissance actions (search/filter/open product pages, probe availability/price boundaries) that can contaminate behavioral signals. With the expected growth of these capabilities, this threat only becomes more perilous to revenue management systems.
|
||||||
|
|
||||||
|
We model an agent session as producing some events with lower in-session conversion levels relative to humans, this we state in our assumption that $P(\text{purchase} \vert A) < P(\text{purchase} \vert H)$ but with a potentially higher volatility in $\hat{q}$, which we observe through the look-to-book metrics in our simulation.
|
||||||
|
|
||||||
|
\subsection{Economic Agents: From Homo Economicus to Machina Economicus}
|
||||||
|
|
||||||
|
Existing behavioral economic models tend to be criticized for the assumption of rational behavior, as is embodied in the term of homo economicus. The definition of a machina economicus by \textcite{parkes_economic_2015} is quite appropriate for our case, particularly because these assumptions of rationality have been argued to be a very adequate reference for AI research by \textcite{varian_economic_1995} due to its expected utility maximizing nature. For modeling this behavior, the trajectories of these agents can be formally defined to be partially observable Markov decision processes \parencite{xie_osworld_2024}. Agents are however not to be confused with web-bots which have previously been known as automated software applications or scrapers which are set with a purpose of carrying out specific tasks on the internet, without a higher level of internal judgement \parencite{imperva_rapid_2025}. In our research, we refer to this actor simply as an Agent belonging to the distribution $A$.
|
||||||
|
|
||||||
|
This economic framing also helps separate two related but distinct phenomena of agents as buyers (changing market demand composition), and agents as information gatherers (changing the observed interactions used by pricing/recommendation systems). The thesis focuses on the second, where information acquisition strategically precedes purchase execution. We do not however dismiss the proposed expectation that existing economic systems serving humans, will not be populated by AIs across multiple channels and with various possibly misaligned goals as stated by \textcite{parkes_economic_2015}.
|
||||||
|
|
||||||
|
A HAP (HTTP Agent Profile) protocol has been developed as an internet draft by \textcite{dhir_http_2025} in an effort to separate agentic and human internet traffic, however the majority adoption by both the sellers and agent providers would be required for the implementation of such a solution.
|
||||||
|
|
||||||
\subsection{Problem Evidence and Market Impact}
|
\subsection{Problem Evidence and Market Impact}
|
||||||
Documented instances of agent-driven market disruptions - Quantitative evidence of pricing manipulation - Case studies from affected industries
|
|
||||||
|
|
||||||
\subsection{Theoretical Foundations: Economic Prallels}
|
The statistical issue of contamination in dynamic pricing systems that observe demand features as a means to update prices has been documented in various previous contexts. The airline industry (which has accounted for 24\% of observed disruptions) has seen malicious activity with a measureable impact on skewing key performance indicators by behavior visible in the look-to-book metrics. Excessive reconnaissance traffic inflates search volume without corresponding completed bookings, thereby skewing demand forecasts and disrupting dynamic pricing models. Demand proxies have also been observed to cause significant threat to inventory management by creating artificial scarcity that distorts the demand-supply relationships in the enterprise model. Censored demand as shown by \textcite{amjad_censored_2017} can also be observed in low-bias demand under-estimation caused by a distortion effect coming from non-human traffic data \parencite{imperva_rapid_2025}.
|
||||||
|
|
||||||
|
When dynamic pricing algorithms operate on highly contaminated or noisy data, the risk grows significantly in creating inaccurate price inferences. The emergent mitigation driven by un-informed reward and regret signals might lead to price suppression for sales continuity which results in harming margins and resulting in a revenue loss. System that poorly fit undesired behavior might result in price gouging, which calls for strong guardrails while preserving targeted business strategy \parencite{mullapudi_reinforcement_2025}.
|
||||||
|
|
||||||
|
|
||||||
|
%Documented instances of agent-driven market disruptions - Quantitative evidence of pricing manipulation - Case studies from affected industries
|
||||||
|
|
||||||
|
\subsection{Theoretical Foundations: Economic Parallels}
|
||||||
|
|
||||||
|
|
||||||
|
Early hints of exploration of prices in a standard English auction explored by \textcite{varian_economic_1995} which hints at exploration of prices in a sequential manner, which leads to a marginally different cost to the bidder than the reservation price of the seller. This is a setting in which there is no cost incured by the buyer for their actions or exploring prices in the market. They propose that any agent responsable for the pricing of a good must be imune to dynamic strategies which might extract private information from a market. A key take-away which relates to the Vickery auction mechanism (also called a \textit{direct mechanism}) suggests that not only would defenses against such exploitation be necessary, but the construction of a mechanism in which revelation of the true willingness to pay is the dominant strategy for commerce.
|
||||||
|
|
||||||
|
Like in classical revenue-maximizing auctions \parencite{roughgarden_cs364a_2013} we assume that the human actor in our system has a private valuation $v$ which we formally draw from intrinsically defined distributions. The important note here is that the agent proxy does not have a mechanism to convey this private information into the demand data which directly impacts the pricing systems.
|
||||||
|
|
||||||
|
The key component of this mediation between agents and commercial platforms lays in the transaction costs related to information gathering and negotiation. As proposed by \textcite{shahidi_coasean_2025} these costs are bound to collapse towards zero (which we demonstrate mathematically), calling for a re-evaluation of the boundaries between firms and markets. As argued by \textcite{coase_nature_1937}, the market participation and time associated with that participation, is critical part of the Coasean transaction cost logic which includes the discovery or relevant pricing within a given market. This process of price discovery without the presence of AI Agents can be time consuming and resource intensive. To build on top of this work we provide a proof of optimal conditions theorised by Coaes as an extension to AI-mediated markets.
|
||||||
|
|
||||||
|
% Economic foundations: relating the problem to options pricing theory. Cost of Information (COI) concept and its relevance
|
||||||
|
|
||||||
|
|
||||||
Economic foundations: relating the problem to options pricing theory. Cost of Information (COI) concept and its relevance
|
|
||||||
|
|
||||||
\subsection{Landscape of Existing Work}
|
\subsection{Landscape of Existing Work}
|
||||||
|
|
||||||
Previous efforts in adversarial computer use LLM agents, show how multi-faceted the whole problem is
|
Explorations of the algorithmic collusion by LLMs \parencite{fish_algorithmic_2025} has demonstrated a cross-model tendency of market division with a strong sensitivity to instructions provided in the ``system prompt''. If a dynamic pricing algorithm which is trained to respond to market signals learns to coordinate with competitor agents (or become manipulated by those agents), the market equilibrium is under threat of destabilization. This is particularly true for Q-learning pricing learners as demonstrated by \textcite{calvano_artificial_2018}.
|
||||||
Here we can show a market visualization (venn-like-diagram)
|
|
||||||
|
Our effort to combat contamination stems from research by \textcite{hardt_strategic_2015} on strategic classification, in conjunction with \textcite{liu_contextual_2024} who demonstrate a linear regret if contamination is ignored. The strategic classification adversarial effect comes from an effort to manipulate some representative features used in a learning pipeline, which can result in lower prices on loans or lower prices from dynamic pricing algorithms.
|
||||||
|
|
||||||
|
To bridge the gap between detection and robust pricing, we look at work in Distributionally Robust Optimization (DRO). As defined by \textcite{kuhn_wasserstein_2024}, DRO provides a framework for decision-making under ambiguity, where the true data distribution is unknown but lies within a ``Wasserstein ball'' of a target distribution. In our context, the ``ambiguity set'' represents the uncertainty introduced by agentic reconnaissance. By optimizing for the worst-case distribution within this set, pricing mechanisms can become resilient to the distributional shifts such as the ones caused by non-human actors, effectively robustifying the revenue function against the contamination described in our problem statement.
|
||||||
|
|
||||||
|
In order to create an environment in which prices can be tested against a demand estimate generated by some behavioral model, we take inspiration from the architecture proposed by \textcite{ie_recsim_2019} in the RecSim platform built for recommendation systems. By modeling the distinct user behavior as POMDPs we can generate faithful interactions which allow us to generalize, past the constraint which is also present in recommendation systems, of rarely having enough experience with individual actor's interactions for good recommendations without generalization. The key inspiration comes from the user choice modeling which we translate to a user transition model for each distinct actor type (agent or human). We further consider the possibility of modeling our quantitative research platform using dynamic Bayesian networks for the sake of tractability within the system. The contribution or RecSim enables researchers to better understand learning algorithms in fixed environments, a gap we identify as needing to be bridged within the space of dynamic pricing.
|
||||||
|
% TODO: mention https://github.com/meta-pytorch/OpenEnv/tree/main/envs/browsergym_env
|
||||||
|
|
||||||
|
We also acknowledge the difficulty in similarly affected fields such as authorship, where \textcite{ganie_uncertainty_2025} demonstrate the theoretical limits of the distributional divergence between text authored by a human or large language model. Their approach of computing the divergence between two distributions demonstrates purely theoretically that no classifier can outperform random guessing on their particular task. This is yet another factor to take into consideration when exploring the potential mitigation strategies.
|
||||||
|
|
||||||
|
The setting of our work is quite complex and covers a wide range of topics, each with its own set of issues that further complicate the task at hand. There is however promise in the field of reinforcement learning and adversarial robustness to combat these problems. We can summarize the characteristics learned from the review of our environment as:
|
||||||
|
\begin{enumerate*}[label=(\roman*)]
|
||||||
|
\item non-stationary demand with temporal noise $\epsilon_t$
|
||||||
|
\item contaminated behavioral signals from mixed human-agent traffic with unknown mixing ratio $\alpha$
|
||||||
|
\item partial observability where only demand proxies $\hat{q}$ are available, not true demand $d(\cdot)$
|
||||||
|
\item strategic actors capable of feature manipulation to influence pricing outcomes
|
||||||
|
\item information asymmetry with private valuations $v$ drawn from unknown distributions
|
||||||
|
\item session-based interactions modeled as POMDPs with trajectories $\tau_s$
|
||||||
|
\item low conversion probability for agents: $P(\text{purchase} \mid A) < P(\text{purchase} \mid H)$
|
||||||
|
\item distributional uncertainty requiring robust optimization within Wasserstein ambiguity sets
|
||||||
|
\item potential for adversarial exploitation through false-name bidding and identity whitewashing.
|
||||||
|
\end{enumerate*}
|
||||||
|
|
||||||
|
|
||||||
|
%Previous efforts in adversarial computer use .LLM agents, show how multi-faceted the whole problem is
|
||||||
|
%Here we can show a market visualization (venn-like-diagram)
|
||||||
|
|||||||
@@ -1,68 +1,456 @@
|
|||||||
\section{Methodology}
|
\section{Methodology}
|
||||||
|
|
||||||
|
% Extra notes and clarifications: we observed some humans and get their transition probabilities between event types
|
||||||
|
% We modify behavioral profiles of transition matrices with price elasticity matrices generated by sample valuations of a distributing.
|
||||||
|
|
||||||
|
This section details the theoretical and practical framework developed to address dynamic pricing under the influence of non-human actors. We begin by formalizing the problem environment and the nature of the actors. We then derive the \textit{Cost of Information} (COI) theorem, proving the erosion of pricing power in the limit of agent saturation. Following this, we outline our generative contamination strategy using GOFAI-driven separability and transition probability learning. Finally, we formulate the robust control problem as a Stackelberg game solved via Distributionally Robust Reinforcement Learning (DR-RL) with constructed ambiguity sets.
|
||||||
|
|
||||||
\subsection{Problem Formalization}
|
\subsection{Problem Formalization}
|
||||||
|
|
||||||
Mathematical formalization of agent-induced pricing distortions. Formal definition of potential loss mechanisms $\alpha D$
|
We define a commercial environment where the platform interacts with a stream of sessions. Let $\mathcal{S}$ denote the set of all sessions. Each session $s \in \mathcal{S}$ is generated by an actor belonging to a latent class $Y_s \in \{H, A\}$, where $H$ denotes Human and $A$ denotes Agent.
|
||||||
|
|
||||||
We consider a business across time during which we have an evolving vector $p_t \in \Re^N$ where $N$ is the number of products in our catalogue. our price vector is directly dependent on a demand function $q_t$ which we define as a linear method of a price elasticity matrix $B_t$. This is the same setup that Microsoft created in their research.
|
Each session produces a trajectory of observable events $\tau_s = (e_{s,1}, \ldots, e_{s,L_s})$. An event $e_{s,k}$ is a tuple defined as:
|
||||||
|
\begin{equation}
|
||||||
|
e_{s,k} = (a_{s,k}, i_{s,k}, t_{s,k})
|
||||||
|
\end{equation}
|
||||||
|
where:
|
||||||
|
\begin{itemize}
|
||||||
|
\item $a_{s,k} \in \mathcal{A}$ is the action taken (e.g., \texttt{view\_item}, \texttt{add\_to\_cart}).
|
||||||
|
\item $i_{s,k} \in \{1, \ldots, N\}$ is the target item index.
|
||||||
|
\item $t_{s,k} \in \mathbb{R}_+$ is the continuous timestamp.
|
||||||
|
\end{itemize}
|
||||||
|
|
||||||
We gether interaction data from users interacting with a sample platform simulating a hotel/airline which generates interaction distributions $I_t = \{(p_t, q_t^\text{obs}, \pi_t)\}_{t=1}^T$
|
The platform does not directly observe the true underlying demand function $d(p)$. Instead, it observes a behavioral proxy $\hat{q}_t$, which is a composite signal derived from the mixture of actor types. We define the demand proxy for product $i$ at epoch $t$ as a weighted aggregation of events:
|
||||||
|
\begin{equation}
|
||||||
|
\label{eq:qhat}
|
||||||
|
\hat{q}_{t,i} = \sum_{s \in \mathcal{S}_t} \sum_{k=1}^{L_s} \omega(a_{s,k}) \cdot \mathbb{1}[i_{s,k} = i]
|
||||||
|
\end{equation}
|
||||||
|
where $\omega: \mathcal{A} \to \mathbb{R}_+$ assigns weights to actions based on their signal strength regarding willingness to pay.
|
||||||
|
|
||||||
|
In the current engine implementation, we use the normalized variant of this proxy for each step:
|
||||||
|
\begin{equation}
|
||||||
|
\tilde q_{t,i} = 100 \cdot \frac{\hat q_{t,i}}{\sum_{j=1}^{N}\hat q_{t,j} + \varepsilon}
|
||||||
|
\end{equation}
|
||||||
|
with fixed category-level weights (cart, dwell, nav, filter) following the same rank order from Table~\ref{tab:action_space}. This keeps the signal dense and directly usable in the simulator.
|
||||||
|
|
||||||
|
\subsubsection{Actor Types and Demand Curves}
|
||||||
|
We formalize the heterogeneity of actors by introducing a type space $\Theta$. An actor of class $Y_s$ is further parameterized by a type $\theta \sim \mathcal{D}_{Y}$. This type determines the actor's demand response function $d(p; \theta)$, sampled from a distribution of possible demand curves. The total observed demand is a stochastic process governed by the naively defined mixture:
|
||||||
|
\begin{equation}
|
||||||
|
\label{eq:mixture_demand}
|
||||||
|
Q(p) = (1-\alpha) \cdot \mathbb{E}_{\theta \sim \mathcal{D}_H}[d(p; \theta)] + \alpha \cdot \mathbb{E}_{\theta \sim \mathcal{D}_A}[d(p; \theta)] + \epsilon_t
|
||||||
|
\end{equation}
|
||||||
|
where $\alpha \in [0, 1]$ represents the contamination parameter (proportion of agents) and $\epsilon_t$ is non-stationary market noise.
|
||||||
|
|
||||||
|
|
||||||
\subsection{Cost of Information Framework}
|
|
||||||
|
|
||||||
Mathematical demonstration and validation of the COI and citation backed evidence, and framework overview + show harm to user via other cost distortions. Maybe split into 3.2.1 (COI Theory) and 3.2.2 (Framework Design)
|
\subsection{Cost of Information (COI) Framework}
|
||||||
|
|
||||||
|
The platform's pricing power comes from information asymmetry: users who express strong interest signals pay more than the base price. We quantify this markup as the \textit{Cost of Information} (COI), which represents the average premium extracted above marginal cost. COI measures the revenue at risk when information asymmetry collapses.
|
||||||
|
A top-level view in the current AI discourse is that sufficiently large productivity gains can induce vertical deflation through cost compression and supply expansion \parencite{rachitsky_marc_2026}. Our contribution is narrower and mechanism-level: even under long-run deflation, platform revenue still depends on short-run information costs to the user. We formalize that rent as the Cost of Information (COI) and study how agentic reconnaissance accelerates its erosion.
|
||||||
|
|
||||||
|
\begin{definition}[Cost of Information]
|
||||||
|
Let $\pi(\tau)$ be a pricing policy mapping interaction histories to prices. The COI is defined as:
|
||||||
|
\begin{equation}
|
||||||
|
\text{COI} = \mathbb{E}[P] - \underline{p}
|
||||||
|
\end{equation}
|
||||||
|
where $\mathbb{E}[P]$ is the expected price charged by the policy and $\underline{p}$ is the minimum viable price (marginal cost).
|
||||||
|
% Alternative survival function representation (used in proof):
|
||||||
|
% COI = \int_{\underline{p}}^{\bar{p}} (1 - F_\pi(p)) \, dp
|
||||||
|
% where F_\pi(p) is the CDF of prices generated by \pi
|
||||||
|
\end{definition}
|
||||||
|
|
||||||
\subsection{System Architecture}
|
|
||||||
\begin{figure}[ht]
|
\begin{figure}[ht]
|
||||||
\centering
|
\centering
|
||||||
\begin{tikzpicture}[
|
\begin{tikzpicture}[scale=1.2]
|
||||||
node distance=1.5cm and 2.5cm,
|
% Define the Gaussian function: centered at 2
|
||||||
box/.style={rectangle, draw, thick, minimum height=1cm, minimum width=3cm, align=center, fill=blue!10},
|
\def\bellcurve(#1){1.5 * exp(-0.5*((#1-2)/0.6)^2)}
|
||||||
kafka/.style={rectangle, draw=orange, thick, minimum height=1cm, minimum width=3cm, align=center, fill=orange!15},
|
|
||||||
arrow/.style={thick,->,>=Stealth}
|
|
||||||
]
|
|
||||||
|
|
||||||
% Nodes
|
% Draw the main axis
|
||||||
\node[box] (webapp) {Web Application \\ (Producer \& Consumer)};
|
\draw[->, thick] (0, 0) -- (4.5, 0) node[right] {$p$};
|
||||||
\node[kafka, below=of webapp] (kafka) {Apache Kafka \\ Cluster};
|
\draw[->, thick] (0, 0) -- (0, 2) node[above] {Density};
|
||||||
\node[box, below=of kafka] (backend) {Backend Services / Microservices \\ (Producers and Consumers)};
|
|
||||||
|
|
||||||
% Connections
|
\draw[thick, smooth, samples=100] plot[domain=0:4] (\x, {\bellcurve(\x)});
|
||||||
\draw[arrow] (webapp) to[out=210,in=150] node[above]{Publish} (kafka);
|
\node at (3.2, 1.2) {$f_\pi(p)$};
|
||||||
\draw[arrow] (kafka) to[out=50,in=330] node[below]{Consume} (webapp);
|
|
||||||
\draw[arrow] (backend) -- node[above]{Publish/Consume} (kafka);
|
|
||||||
|
|
||||||
% Optional: Kafka internal components
|
% Define p_min and E[p]
|
||||||
%\node[below=0.7cm of kafka, align=center] (topics) {Topics \\ Partitions};
|
\def\pmin{0.8}
|
||||||
|
\def\mean{2}
|
||||||
|
|
||||||
% Optional background
|
% Vertical lines
|
||||||
\begin{scope}[on background layer]
|
\draw[dashed] (\pmin, 0) -- (\pmin, 2.0);
|
||||||
\node[draw, rounded corners, fill=orange!5, fit=(kafka), inner sep=0.3cm] {};
|
\draw[dashed] (\mean, 0) -- (\mean, 2.0);
|
||||||
\end{scope}
|
|
||||||
\end{tikzpicture}
|
% Labels on axis
|
||||||
\caption{Technical Diagram}
|
\node[below] at (\pmin, 0) {$\underline{p}$};
|
||||||
|
\node[below] at (\mean, 0) {$\mathbb{E}[p]$};
|
||||||
|
|
||||||
|
\draw[<->, thick, red] (\pmin, 2.0) -- (\mean, 2.0) node[midway, above] {COI};
|
||||||
|
|
||||||
|
\end{tikzpicture}
|
||||||
|
\caption{Illustration of the Cost of Information (COI). The COI is defined as the difference between the expected price $\mathbb{E}[p]$ realized by the policy and the minimum viable price $\underline{p}$.}
|
||||||
|
\label{fig:coi_illustration}
|
||||||
\end{figure}
|
\end{figure}
|
||||||
|
|
||||||
High level overview of how it works
|
We now formally demonstrate that standard dynamic pricing mechanisms are not incentive-compatible with high-frequency agentic traffic. As the number of independent competitive agents $N$ querying the system grows, the platform's ability to sustain a COI vanishes.
|
||||||
|
|
||||||
|
A fundamental assumption for our claim lies in the alignment of the AI agent through its prompt which has been demonstrated by \cite{fish_algorithmic_2025} to cause strong collusive behavior under linguistic nudges. This assumption can be generalized to the human user asking the agent to research products with a minimizing objective.
|
||||||
|
|
||||||
|
\begin{theorem}[COI Erosion in the Limit]
|
||||||
|
Let $N$ be the number of independent, utility-maximizing agents querying the platform. Let $p_{(1)}$ be the first order statistic (minimum) of the prices offered to these agents. As $N \to \infty$, the Cost of Information converges to 0.
|
||||||
|
\end{theorem}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
\begin{proof}
|
||||||
|
Consider $N$ independent agents querying the platform, each receiving a price sample $p_i$ drawn from the pricing policy's distribution $F(p)$ bounded by $[\underline{p}, \bar{p}]$. A strategic agent conducting reconnaissance will select the minimum observed price: $p_{(1)} = \min(p_1, \ldots, p_N)$.
|
||||||
|
% support here means that its the range of possible outputs.
|
||||||
|
The probability that the minimum price exceeds some threshold $t$ is:
|
||||||
|
\begin{equation}
|
||||||
|
P(p_{(1)} > t) = P(\text{all } p_i > t) = [1 - F(t)]^N
|
||||||
|
\end{equation}
|
||||||
|
|
||||||
|
For any price $t > \underline{p}$, the CDF satisfies $F(t) > 0$, so $1 - F(t) < 1$. As $N$ grows, this probability decays exponentially: $[1 - F(t)]^N \to 0$.
|
||||||
|
|
||||||
|
The expected minimum price can be written as:
|
||||||
|
\begin{equation}
|
||||||
|
\mathbb{E}[p_{(1)}] = \underline{p} + \int_{\underline{p}}^{\bar{p}} [1 - F(t)]^N \, dt
|
||||||
|
\end{equation}
|
||||||
|
|
||||||
|
Since the integrand vanishes as $N \to \infty$ for all $t > \underline{p}$, the integral converges to zero. Therefore:
|
||||||
|
\begin{equation}
|
||||||
|
\lim_{N \to \infty} \text{COI} = \lim_{N \to \infty} (\mathbb{E}[p_{(1)}] - \underline{p}) = 0
|
||||||
|
\end{equation}
|
||||||
|
\end{proof}
|
||||||
|
|
||||||
|
|
||||||
|
This result naively proves that standard pricing policies $\pi$ fail to extract surplus in the presence of large-scale agentic search, necessitating a robust counter-mechanism.
|
||||||
|
|
||||||
|
% The DRO objective creates a lower bound on COI extraction, effectively guaranteeing a minimum margin even in the presence of adversarial agents. we need to prove this and demonstrate that in a theorem.
|
||||||
|
|
||||||
|
|
||||||
|
%Mathematical demonstration and validation of the COI and citation backed evidence, and framework overview + show harm to user via other cost distortions. Maybe split into 3.2.1 (COI Theory) and 3.2.2 (Framework Design)
|
||||||
|
|
||||||
|
\subsection{System Architecture: Hybrid Kappa-Lambda Architecture}
|
||||||
|
|
||||||
|
In order for our research to have grounding in interactions we built a robust e-commerce web-platform. We initially conducted a survey of the leading platforms of airlines and hotel booking sites to identify the specific interface patterns that effectively manage complex travel data. Our analysis revealed a clear industry standard: while both sectors rely on tabbed service selection and left-sidebar filtering to streamline navigation, they diverge in result presentation: airlines utilize visual date-price bars and multi-step wizards to optimize for logistical transparency, whereas hotel platforms leverage image-led cards and scarcity triggers to drive emotional engagement and urgency. Our web framework defines a highly agnostic boilerplate which can be seeded with any data-modality with an easy-to-tailor pattern, which we leverage to define a \texttt{hotel} and \texttt{airline} mode. Both modes are then individually deployed via an environment level argument which adjusts the proxy routing with a custom middleware inside next.js to render only the desired mode. The purpose of this was to create a baseline adaptable to any use-case or desired commercial application.
|
||||||
|
|
||||||
|
The architecture of this platform begins with the deployed web-apps posting interaction data to our backend which processes them and stores each ingested interaction into a kafka cluster. This serves as our data reservoir tracking and associating each interaction with its session and importantly with which experiment it belongs to. Not only do we track the behavioral interactions, but our pricing provider micro-service, once called by the frontend reports the observed/queried price-product into kafka. This kafka cluster is subscribed to by our pipeline which is configured on a schedule in Airflow, with the possibility of manual trigger. The final stage of the pricing pipeline, submits computed dynamic pricing results into a redis database for quick updates which is then read by the pricing provider and displayed on the webapp. This is a very generic end-to-end mechanism which is applicable to a variety of different e-commerce tasks. We intentionally put emphasis on the development of this infrastructure to establish a reproducible framework for interaction and to minimize any noise.
|
||||||
|
|
||||||
|
\paragraph{Public Web Artifact} We transition the Kappa like architecture of the data collection to a Lambda architecture for actual learning in a surrogate environment. This allows us to move faster on data which is provided and helps us create a feedback loop for production deployment. To support further research in this intersection of fields we release P4P \footnote{\url{https://github.com/velocitatem/p4p}} as a public repository providing the interaction layer of the PHANTOM framework. This provides a configurable storefront which can be tailored to any commercial setting with a standardized session-level event tracking. We document the API adapters or what the framework expects in terms of schemas for pricing providers and log ingestion servicse. The repository is intended for controlled experimentation and method replication rather than production commerce deployment.
|
||||||
|
|
||||||
|
|
||||||
|
\subsubsection{DevOps Principles}
|
||||||
|
|
||||||
|
Reproducible results are key to quality research platforms, this is taken into mind when deploying and working with our research platform. From a deployment standpoint the platform can be deployed across a large variety of providers and can be run locally. When developing a new interaction modality apart from the ones that come out of the box, a simple template pattern can be followed. The middleware of the framework is designed to properly render the chosen modality from environmental variables, thus deployment of different or parallel version of the software can be easily parametrized.
|
||||||
|
|
||||||
|
\subsubsection{Online Dynamic Pricing}
|
||||||
|
|
||||||
|
In order to collect data from actors under correct conditions we replicate a naive and simple dynamic pricing algorithm which runs in the background during the experiments.
|
||||||
|
The dynamic pricing done is handled by a pipeline which computes a demand estimate on a per-product basis of a specific window of the data, defined by the period $T$ which by default is 5 minutes. This dynamic pricing pipeline computes a demand estimate vector $\hat{q} \in \mathbb{R}^N$ by a weighted sum of interactions for each product, it additionally computes a price elasticity vector $\hat{\epsilon}$ in the same dimensions as our demand. The final features matrix is of the size $N \times 2$ which we translate to a new price vector $\hat{p} \in \mathbb{R}^N$. The transformation that governs this dynamic pricing is a very simple surge-based pricing (a special case of our later defined policy $\pi$):
|
||||||
|
|
||||||
|
\begin{equation}
|
||||||
|
\hat{p}_i = \begin{cases}
|
||||||
|
p_{0,i} \cdot \lambda_{\text{surge}} & \text{if } \hat{q}_i \geq \theta_{\text{high}} \\
|
||||||
|
p_{0,i} \cdot \lambda_{\text{disc}} & \text{if } \hat{q}_i \leq \theta_{\text{low}} \\
|
||||||
|
p_{0,i} & \text{otherwise}
|
||||||
|
\end{cases}
|
||||||
|
\quad \forall i \in \{1, \ldots, N\}
|
||||||
|
\end{equation}
|
||||||
|
|
||||||
|
where $p_0 \in \mathbb{R}^N$ is the base price vector (which is seeded into our database distinctly for each mode of the commerce platform), $\theta_{\text{high}}, \theta_{\text{low}} \in \mathbb{R}$ are demand thresholds defining surge and discount regions, and $\lambda_{\text{surge}}, \lambda_{\text{disc}} \in \mathbb{R}^+$ are multiplicative factors with typical values $\lambda_{\text{surge}} = 1.2$ and $\lambda_{\text{disc}} = 0.9$. This piecewise function enables rapid price adjustment in response to observed demand without requiring complex elasticity estimation or historical calibration, allowing us to expose actors within our experiments to a system with a dynamic component of pricing.
|
||||||
|
|
||||||
|
% For our offline experimental setting, we generalize a master value function that can encompass different demand estimation and pricing strategies.
|
||||||
|
%
|
||||||
|
% \begin{align}
|
||||||
|
% V(\cdot) = \max_{p_t} \min_{Q \in \mathcal{U}(\hat{d})}{\mathbb{E}_{d\sim Q} [p_t \times d(p_t, x_t ; \theta) + \psi V_{t+1}(\cdot)]}
|
||||||
|
% \end{align}
|
||||||
|
%
|
||||||
|
% We evaluate different substitutions of this objective, which later serve as hyperparameters in the simulator.
|
||||||
|
|
||||||
\subsection{Experimental Design}
|
\subsection{Experimental Design}
|
||||||
Study methodology and approach. Data acquisition strategy. Defined objectives and success criteria. Observable metrics and KPIs
|
|
||||||
|
|
||||||
\subsection{Dynamic Pricing Algorithm Analysis}
|
We start from a practical constraint: we do not have access to proprietary production data. Because of that, we design our own fictional platform that still represents how commercial platforms work in the real world. The design comes from a survey of hotel and airline websites, where we extracted common interface components and used them as a high-level template for dynamic pricing environments.
|
||||||
Deep dive into how the algorithm works, different kinds and justification for chosen appraoches + agent impact modeling and quantification.
|
|
||||||
\subsection{Reinforcement Learning Formulation}
|
|
||||||
How do we define the state space, action space and reward function breakdown and algorithm benchmarking.
|
|
||||||
POSSIBLY: Expand into full subsections: 3.6.1 (State-Action Space), 3.6.2 (Reward Design), 3.6.3 (Benchmarking)
|
|
||||||
|
|
||||||
|
The interface is organized as a product catalog where each product belongs to a time-bounded price vector (for example, a daily pricing period). During each period we collect interaction data by instrumenting UI components and predefined action templates that are still customizable. This gives us control without losing realism.
|
||||||
|
|
||||||
|
Since users act with motivations, we define a pool of tasks (jobs to be done) and assign tasks randomly to participants.
|
||||||
|
% TODO: describe the task pool in detail here -- list the specific tasks used in the experiments
|
||||||
|
A representative task is to find the cheapest feasible catalog item under explicit constraints while removing strict financial limits so we avoid trivial optimization behavior. Participants are also randomly assigned to one experimental platform mode (hotel or airline). Once assigned, they are dropped into the experiment with an actor ID. Under each experiment ID, we can observe multiple sessions across time and gather long interaction traces for the same actor.
|
||||||
|
|
||||||
|
The human data collection involved 18 participants, all of whom provided explicit informed consent prior to their session. Participants had an average age of 21 years and were recruited from a university population. Alongside the 18 human sessions we ran 18 agent sessions of equivalent task scope, giving a balanced dataset of 36 labeled trajectories. Each participant was assigned a single platform mode and a single task drawn from the pool, and completed the session independently without guidance on navigation or pricing strategy.
|
||||||
|
|
||||||
|
To evaluate quality and realism of the setup, we store both structured event logs and full interaction transcripts. This lets us combine quantitative analysis with transcript-level qualitative findings. The result is an isolated system where we can control the interaction process while preserving realistic behavior.
|
||||||
|
|
||||||
|
Operationally, goals and experiment runs are tracked in PostgreSQL (goal table, run table, and assignment mapping). This data-acquisition phase is the first half of the methodology and is intentionally a disconnected component that feeds the later contributions. The second half uses collected behavioral traces to separate classes $y \in \{A,H\}$ with session-conditioned probability estimates, then injects those estimates into the pricing learner.
|
||||||
|
|
||||||
|
Our process follows three stages: (1) observe and \textit{vectorize} behavioral interactions, (2) learn separability to characterize human versus agent patterns, and (3) use the learned signal to train a defensive policy in a controlled dynamic-pricing simulator.
|
||||||
|
|
||||||
|
\begin{figure}[ht]
|
||||||
|
\resizebox{\columnwidth}{!}{%
|
||||||
|
\input{chapters/loop_figure.tex}
|
||||||
|
}
|
||||||
|
\caption{Overview of the Dynamic Pricing Tasks.}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
Our web platform (developed in similar spirit to RecSim \parencite{ie_recsim_2019}) gives us a controlled environment where tasks are assigned to human and agentic actors and then executed. Each actor receives a browser-level experiment identifier that may persist across multiple session IDs. We then group by experiment and extract session trajectories using the schema below.
|
||||||
|
|
||||||
|
To speak to realism, user interviews reported that the platform architecture mirrored standard booking interfaces and reduced the cognitive load required to learn the system. One participant described the flow as ``intuitive'' and close to a ``normal'' transaction, suggesting observed behavior was primarily driven by pricing treatment rather than interface novelty.
|
||||||
|
|
||||||
|
The dynamic pricing mechanism elicited immediate behavioral adjustments. Participants were sensitive to price volatility: sudden boosts triggered urgency and faster booking attempts, while large listing-to-final discrepancies triggered deeper comparison behavior. This is comforting because the controlled setup still produces commercially relevant interaction data.
|
||||||
|
|
||||||
|
|
||||||
|
\subsubsection{Design of Training Factorial Study}
|
||||||
|
|
||||||
|
The simulator has multiple configurable factors. We design a multi-factor study across five axes derived from the sweep configurations: (1) RL algorithm (\texttt{ppo}, \texttt{a2c}, \texttt{dqn}, \texttt{qtable}; 4 levels), (2) contamination ratio $\alpha$ sampled from $[0.1, 0.6]$ at four representative levels, (3) robustness radius $\epsilon_\alpha \in \{0.0, 0.15, 0.3\}$ (3 levels), (4) COI penalty weight $\lambda_\text{coi}$ at two reference levels, and (5) pricing action granularity (two discretization settings for \texttt{action\_levels}); giving a grid of $4\times4\times3\times2\times2 = 192$ configurations. Statistical power for the behavioral comparisons is determined by a two-sample test over per-session KL divergence scores; a formal power analysis with minimum detectable effect size at $n=18+18$ is reported in the results.
|
||||||
|
% Power analysis plan: apply a two-sample Mann-Whitney U (or permutation test) on per-session (delta_H - delta_A) divergence scores comparing the human and agent groups. Compute minimum detectable effect size at alpha=0.05, power=0.8, given n=18 per group. Bootstrap confidence intervals on mean KL are a cleaner complement given the non-normality of divergence distributions.
|
||||||
|
While this scale is generally expensive for reinforcement learning, we execute it on a large TPU cluster to make the sweep tractable.
|
||||||
|
|
||||||
|
Our training budget is provisioned through TPU Research Cloud and spans 384 chips across TPU v4, v5e, and v6e generations, with a spot-heavy allocation plus an on-demand reserve. At peak BF16 throughput this corresponds to approximately 160 PFLOPS of aggregate compute, which makes repeated seeds, ablations, and sensitivity sweeps feasible within practical wall-clock limits. We allocate v6e capacity to the highest-intensity policy training jobs, use v5e for wider hyperparameter exploration where throughput-per-dollar is favorable, and reserve on-demand v4 capacity for runs that should not be interrupted.
|
||||||
|
|
||||||
|
\begin{table}[ht]
|
||||||
|
\centering
|
||||||
|
\caption{Compact comparison of TPU generations used in the training stack.}
|
||||||
|
\label{tab:tpu_specs}
|
||||||
|
\begin{tabular}{@{}llll@{}}
|
||||||
|
\toprule
|
||||||
|
\textbf{Feature} & \textbf{TPU v4} & \textbf{TPU v5e} & \textbf{TPU v6e (Trillium)} \\
|
||||||
|
\midrule
|
||||||
|
Peak BF16 per chip (TFLOPS) & 275 & 197 & 918 \\
|
||||||
|
HBM capacity per chip (GB) & 32 & 16 & 32 \\
|
||||||
|
HBM bandwidth per chip (GB/s) & 1200 & 819 & 1600 \\
|
||||||
|
TensorCores per chip & 2 & 1 & 1 \\
|
||||||
|
Interconnect topology & 3D mesh/torus & 2D torus & 2D torus \\
|
||||||
|
Max pod size (chips) & 4096 & 256 & 256 \\
|
||||||
|
\bottomrule
|
||||||
|
\end{tabular}
|
||||||
|
\end{table}
|
||||||
|
|
||||||
|
\begin{table}[ht]
|
||||||
|
\centering
|
||||||
|
\caption{TPU allocation used for the factorial study.}
|
||||||
|
\label{tab:tpu_allocation}
|
||||||
|
\begin{tabular}{@{}llll@{}}
|
||||||
|
\toprule
|
||||||
|
\textbf{TPU Type} & \textbf{Total Chips} & \textbf{Zone(s)} & \textbf{Provisioning} \\
|
||||||
|
\midrule
|
||||||
|
v6e & 128 (64 + 64) & europe-west4-a, us-east1-d & Spot \\
|
||||||
|
v5e & 128 (64 + 64) & us-central1-a, europe-west4-b & Spot \\
|
||||||
|
v4 & 64 (32 + 32) & us-central2-b & 32 Spot + 32 On-demand \\
|
||||||
|
\bottomrule
|
||||||
|
\end{tabular}
|
||||||
|
\end{table}
|
||||||
|
|
||||||
|
For connections from Madrid, we prioritize the europe-west4 allocation for latency-sensitive runs with the benefit of having the most grouped chips within a single region. This regional grouping is important for the deployment of our Kubernetes cluster which cannot span multiple regions. All sweep metadata, model checkpoints, and reward traces are logged in Weights \& Biases. Hardware specifications are from the official Google Cloud TPU documentation \parencite{noauthor_tpu_2026,noauthor_tpu_2025-1,noauthor_tpu_2025}.
|
||||||
|
|
||||||
|
Design of training processes: we build docker image with the fact in mind of different caching over layers in order to most speed up docker re-building and such we place the most volatile steps towards the end of the image building. What is means in practice is that any dependency installations are isolated so edits to source code do no trigger rebuilds. Only if we update our entry point of training a sweep, Docker will also rebuild the source-code copy stage.
|
||||||
|
|
||||||
|
Due to the preemptive nature of the current demand of TPU chips we sttle for running our on demeaned as the primary source of compute. The on demand TPU pod of 32 chips spread across 4 virtual hosts creates a relatively unique parallelization setup. Despite our desire to use a traditional approach of clustering and perhaps deploying SLURM jobs of our sweep agent, the lack of predictability in provisioning each instance of a compute resource makes this an high friction layer we do not want to add.
|
||||||
|
|
||||||
|
\subsubsection{Interaction Schema}
|
||||||
|
|
||||||
|
We extend the basic event tuple $e_{s,k}$ to capture the full observational signal available to the platform. An interaction event is defined as the extended tuple:
|
||||||
|
\begin{equation}
|
||||||
|
e_{s,k} = \left( a_{s,k}, \, i_{s,k}, \, t_{s,k}, \, \mu_{s,k}, \, \delta_{s,k} \right)
|
||||||
|
\end{equation}
|
||||||
|
where $\mu_{s,k} \in \mathcal{M}$ is a metadata record containing action-specific context (e.g., price observed, filter parameters, element text), and $\delta_{s,k} \in \mathbb{R}_+$ is the dwell time in milliseconds for attention-based actions.
|
||||||
|
|
||||||
|
A session $s$ is itself a structured record:
|
||||||
|
\begin{equation}
|
||||||
|
s = \left( \text{sid}, \, \text{eid}, \, t_0, \, \phi, \, \mathcal{U}, \, \tau_s \right)
|
||||||
|
\end{equation}
|
||||||
|
where $\text{sid}$ is a unique session identifier (UUID), $\text{eid}$ optionally links to an experiment, $t_0$ is the session start timestamp, $\phi \in \{\texttt{hotel}, \texttt{airline}\}$ denotes the platform mode, $\mathcal{U}$ is the user-agent string, and $\tau_s$ is the trajectory of events.
|
||||||
|
|
||||||
|
The action space $\mathcal{A}$ is partitioned into four semantic categories based on the behavioral signal each action conveys:
|
||||||
|
|
||||||
|
\begin{table}[ht]
|
||||||
|
\centering
|
||||||
|
\caption{Action space partition $\mathcal{A} = \mathcal{A}_{\text{nav}} \cup \mathcal{A}_{\text{cart}} \cup \mathcal{A}_{\text{filter}} \cup \mathcal{A}_{\text{dwell}}$ with signal interpretation.}
|
||||||
|
\label{tab:action_space}
|
||||||
|
\begin{tabular}{@{}llll@{}}
|
||||||
|
\toprule
|
||||||
|
\textbf{Category} & \textbf{Actions} & \textbf{Signal} & $\boldsymbol{\omega}$ \\
|
||||||
|
\midrule
|
||||||
|
$\mathcal{A}_{\text{cart}}$ & \texttt{add\_item}, \texttt{remove}, \texttt{checkout}, \texttt{purchase} & Purchase intent & High \\
|
||||||
|
$\mathcal{A}_{\text{dwell}}$ & \texttt{hover\_title}, \texttt{hover\_paragraph}, \texttt{hover\_link} & Sustained attention & Medium \\
|
||||||
|
$\mathcal{A}_{\text{nav}}$ & \texttt{page\_view}, \texttt{view\_item}, \texttt{learn\_more} & Discovery & Low \\
|
||||||
|
$\mathcal{A}_{\text{filter}}$ & \texttt{search}, \texttt{filter\_date}, \texttt{filter\_price}, \texttt{sort} & Preference refinement & Lowest \\
|
||||||
|
\bottomrule
|
||||||
|
\end{tabular}
|
||||||
|
\end{table}
|
||||||
|
|
||||||
|
This partition enables the weight function $\omega$ from Eq.~\ref{eq:qhat} to assign category-specific signal strengths, with $\omega(\mathcal{A}_{\text{cart}}) > \omega(\mathcal{A}_{\text{dwell}}) > \omega(\mathcal{A}_{\text{nav}}) > \omega(\mathcal{A}_{\text{filter}})$ reflecting decreasing commitment.
|
||||||
|
|
||||||
|
In the simulator baseline this order is encoded with a compact fixed scale: cart $=4.0$, dwell $=2.0$, nav $=1.0$, filter $=0.5$. Unknown actions are mapped by prefix heuristics to the nearest category.
|
||||||
|
|
||||||
|
The metadata record $\mu$ varies by action type. For product views, $\mu$ contains the observed price $p_{\text{obs}}$ and product attributes. For dwell events, $\mu$ includes the element text and accumulated hover duration. This heterogeneous structure is captured via a schema-on-read approach in our Kafka ingestion pipeline, where events are validated against type-specific schemas before storage.
|
||||||
|
|
||||||
|
In addition to behavioral events, the platform logs price observations to a separate Kafka topic. Each price query generates a record $(i, p, \text{sid}, \phi, t)$ associating the product, displayed price, requesting session, platform mode, and timestamp. This dual-stream architecture enables joint analysis of price exposure and behavioral response.
|
||||||
|
|
||||||
|
|
||||||
|
\subsection{Generative Contamination and Separability}
|
||||||
|
|
||||||
|
To train a robust pricing learner, we need a simulator that can generate realistic interaction data under controlled contamination. We build this from Phantom data using a two-stage approach.
|
||||||
|
|
||||||
|
|
||||||
|
\subsubsection{Ground-Truth Separability}
|
||||||
|
Because sessions are collected under controlled experimental conditions where each actor is assigned a known type at the start of the trial, labels $y_s \in \{H, A\}$ are available as ground truth rather than as the output of a heuristic classifier. We therefore estimate separate transition kernels directly from each labeled partition $\mathcal{D}_H$ and $\mathcal{D}_A$, treating the resulting $\hat{\mathcal{T}}_H$ and $\hat{\mathcal{T}}_A$ as the ground-truth behavioral profiles for each class. We then ask a direct methodological question: are the kernels separable enough to justify downstream pricing control that depends on that separability?
|
||||||
|
|
||||||
|
To answer this, we compute average KL divergence between transition probability matrices. This statistic gives global separability and event-level diagnostics at the same time. In our balanced dataset (50\% human, 50\% agent), the average divergence is approximately $1.8$. To contextualize this divergence metric we compare with an intra-class comparison baseline of randomly selected transitions.
|
||||||
|
% To contextualize this figure a useful intra-class baseline is to randomly split D_H into two equal halves, estimate a kernel from each half, compute the same average KL statistic, and repeat for B bootstrap samples (e.g. B=100). The resulting null distribution (mean +/- std) gives the divergence expected purely from estimation noise at this sample size. A between-class KL substantially above this null confirms the separation is real and not a finite-sample artefact. In practice: for each of B splits, partition D_H 50/50 without replacement, run build_kernel() on each half, average the per-state KL values, and collect the B scores into a reference distribution to compare against the 1.8 figure.
|
||||||
|
|
||||||
|
\begin{definition}[Kullback-Leibler Divergence for Transition Distributions]
|
||||||
|
Let $P_e$ and $Q_e$ be categorical distributions over destination states following event $e$, derived from human and agent trajectories respectively. The KL divergence between these distributions is:
|
||||||
|
\begin{equation}
|
||||||
|
D_{\mathrm{KL}}(P_e \parallel Q_e) = \sum_{k \in \mathcal{S}_e} P_e(k) \log \frac{P_e(k)}{Q_e(k)}
|
||||||
|
\end{equation}
|
||||||
|
where $\mathcal{S}_e$ denotes the set of destination events that follow $e$ in the human trajectories.
|
||||||
|
\end{definition}
|
||||||
|
|
||||||
|
To obtain this statistic, we aggregate transitions by triggering event $e$ and treat normalized outgoing probabilities as categorical distributions $P_e$ (human) and $Q_e$ (agent). We intersect shared event labels, then accumulate log-ratio contributions over shared destinations. Large contributions, including near-zero $Q_e(k)$ cases, identify transitions where one actor class is difficult to mimic.
|
||||||
|
|
||||||
|
With these divergence features we train a contrastive model to estimate a weak agent probability $f(\tau)\in[0,1]$, which we later use as a weighting and control signal.
|
||||||
|
|
||||||
|
|
||||||
|
\subsubsection{Transition Probability Estimation}
|
||||||
|
\label{sec:tpe}
|
||||||
|
|
||||||
|
|
||||||
|
For both subsets, we model session dynamics as an MDP and estimate transition kernel $\mathcal{T}$. For each actor type we estimate global kernels $\hat{\mathcal{T}}_A$ and $\hat{\mathcal{T}}_H$, then cluster into behavioral sub-kernels $\hat{\mathcal{T}}_y^i$ to avoid collapsing all behavior into one average profile. Transition probabilities are estimated by maximum likelihood:
|
||||||
|
\begin{equation}
|
||||||
|
\hat{P}(s' \mid s) = \frac{N(s, s')}{\sum_{k \in \mathcal{S}} N(s, k)}
|
||||||
|
\end{equation}
|
||||||
|
where $N(s, s')$ is the observed transition count. This allows us to construct a \textit{Contamination Generator} $\mathcal{G}(\alpha)$. Given a clean trajectory dataset, $\mathcal{G}$ injects synthetic agent trajectories sampled from $\hat{\mathcal{T}}_A$ until the effective mixing ratio reaches $\alpha$. The properties of an MDP such as ... should be preserved by the operation described below.
|
||||||
|
|
||||||
|
To scale this to catalog-level pricing, we expand the base event transition matrix from $T\times T$ into product-specific transitions using the current demand condition. In practice, we normalize the demand vector across products and use it to weight how much transition mass each product pair receives. Concretely, each cell of the base matrix becomes an $N\times N$ block (for $N$ products), so the transition matrix grows from $T\times T$ to $(T\cdot N)\times(T\cdot N)$. Finally, we add $C$ generic states (homepage, login, checkout terminal states), which gives the full kernel size $(T\cdot N + C)\times(T\cdot N + C)$.
|
||||||
|
% The validity of this demand-weighted block expansion is still subject to formal proof: it needs to be shown that the resulting matrix retains row-stochasticity (rows summing to 1) and that the weighting by the demand vector preserves the Markov property for the expanded state space. In the engine source this is the target of ongoing validation before the expansion is relied on for behavioral generation at scale.
|
||||||
|
|
||||||
|
\begin{figure}[ht]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.8\textwidth]{chapters/mdp_human.pdf}
|
||||||
|
\caption{Markov Decision Process visualization illustrating the behavioral transition dynamics for \textbf{human} actions.}
|
||||||
|
\label{fig:human_mdp_viz}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
\begin{figure}[ht]
|
||||||
|
\centering
|
||||||
|
\includegraphics[width=0.8\textwidth]{chapters/mdp_agent.pdf}
|
||||||
|
\caption{Markov Decision Process visualization illustrating the behavioral transition dynamics for \textbf{agent} behavior profiles. The state space and transition probabilities are learned from observed session trajectories to enable generative contamination.}
|
||||||
|
\label{fig:agent_mdp_viz}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
|
||||||
|
\subsection{Second-Stage Classification}
|
||||||
|
After contamination, we run a second classification stage. We remap events into a semantically aligned feature space, apply richer feature engineering, and retrain to obtain cleaner label probabilities across the full dataset. This classifier is then used directly in the reinforcement-learning reward structure.
|
||||||
|
|
||||||
|
\subsection{Distributionally Robust Reinforcement Learning (DR-RL)}
|
||||||
|
|
||||||
|
We formulate pricing as a Stackelberg game: the platform (leader) sets prices $p_t$, and the population (follower) responds through trajectories and demand. A useful intuition is that the platform behaves like a distorted mirror at a 45-degree angle: what it mirrors is population demand into an estimated demand proxy, and that proxy drives revenue.
|
||||||
|
|
||||||
|
Because contamination level $\alpha$ and demand shift are non-stationary online, a simple error term is not enough. We therefore use a Distributionally Robust Optimization objective. Let $\tau'$ be a newly observed trajectory generated by an unknown actor profile (sampled from the behavioral models in Section~\ref{sec:tpe}). We need a demand mapping conditioned on price and trajectory, $\hat{Q}(p,\tau')$. For each $\tau'$, we compute $\hat{\mathcal{T}}'$ and compare it with controlled baselines $\bar{\mathcal{T}}_H$ and $\bar{\mathcal{T}}_A$:
|
||||||
|
|
||||||
|
\begin{align}
|
||||||
|
\label{eq:delta_H}
|
||||||
|
\Delta_H &= D_{KL}(\hat{\mathcal{T}}^\prime \parallel \bar{\mathcal{T}}_H) \\
|
||||||
|
\label{eq:delta_A}
|
||||||
|
\Delta_A &= D_{KL}(\hat{\mathcal{T}}^\prime \parallel \bar{\mathcal{T}}_A)
|
||||||
|
\end{align}
|
||||||
|
|
||||||
|
This yields two centroid-like heuristics that act as a session-level agent score in the engine. On a per-customer or use-case basis a similar study should be done in order to obtain ground truth behavior models for humans and agents and their specific interaction with a given products website.
|
||||||
|
|
||||||
|
In implementation, we maintain an alternating game-history stack (our \textit{Limbo} stack) and execute it explicitly every epoch with exactly two transitions: first the platform publishes a price vector (leader move), then the market responds with trajectory-derived demand (follower move).
|
||||||
|
|
||||||
|
% Mention discretized action space and the clipping and over shotting in continuous action spaces
|
||||||
|
% Also talk about catastrophic economics, we add termination on bankrupcy or zero demand so market collaps
|
||||||
|
|
||||||
|
\subsubsection{Ambiguity Set Construction}
|
||||||
|
We define an ambiguity set $\mathcal{U}_\epsilon(\hat{P}_N)$ centered around our empirical reference distribution $\hat{P}_N$ (derived from the generator $\mathcal{G}$). We utilize the Wasserstein distance metric to define the set of plausible demand distributions the agent might face:
|
||||||
|
\begin{equation}
|
||||||
|
\mathcal{U}_\epsilon(\hat{P}_N) = \left\{ Q \in \mathcal{P}(\Xi) : W_p(Q, \hat{P}_N) \le \epsilon \right\}
|
||||||
|
\end{equation}
|
||||||
|
This set captures all distributions that are statistically close to our observed training data but allows for adversarial shifts.
|
||||||
|
|
||||||
|
For the current engine baseline, we use a compact inner-robust approximation by applying ambiguity over contamination in a local interval around nominal contamination $\alpha_0$:
|
||||||
|
\begin{equation}
|
||||||
|
\mathcal{A}_{\epsilon_\alpha}(\alpha_0)=\left\{\alpha\in[0,1]:\lvert\alpha-\alpha_0\rvert\le\epsilon_\alpha\right\}
|
||||||
|
\end{equation}
|
||||||
|
and we evaluate a small fixed grid in $\mathcal{A}_{\epsilon_\alpha}(\alpha_0)$ per step, selecting the worst-case candidate for the learner.
|
||||||
|
% A proper Wasserstein ball implementation over the full demand distribution (rather than a scalar alpha interval) would use the POT library (Python Optimal Transport): compute W_2 between the empirical reference P_hat and each candidate Q using ot.emd2() or ot.sliced_wasserstein_distance() for scalability, then accept only candidates within epsilon. In practice the inner minimization becomes: candidates = [G(alpha) for alpha in linspace]; dists = [ot.emd2(p_hat, q, M) for q in candidates]; worst = candidates[argmin(reward[dists <= epsilon])]. The current grid-on-alpha approximation is a computationally cheap substitute; moving to a true Wasserstein ball would tighten the worst-case guarantee but requires specifying the ground metric M over the demand space.
|
||||||
|
|
||||||
|
\subsubsection{The Min-Max Objective}
|
||||||
|
The robust policy $\pi^*$ is obtained by solving the maximin problem:
|
||||||
|
\begin{equation}
|
||||||
|
\label{eq:robust_policy}
|
||||||
|
\pi^* = \arg \max_{\pi} \min_{Q \in \mathcal{U}_\epsilon} \mathbb{E}_{d \sim Q} \left[ R(p, d) - \lambda \cdot \text{COI}_{\text{leak}}(p,\tau') \right]
|
||||||
|
\end{equation}
|
||||||
|
where $R(p, d)$ is the revenue function and $\lambda$ weighs the information-leakage penalty.
|
||||||
|
|
||||||
|
In practice, we parameterize this with a session-level leakage term:
|
||||||
|
\begin{equation}
|
||||||
|
\text{COI}_{\text{leak}}(p,\tau') = f(\tau')\cdot \text{InfoValue}(p,\tau')
|
||||||
|
\end{equation}
|
||||||
|
where $f(\tau')$ is the weak agent probability and $\text{InfoValue}$ is implemented either as a constant query-tax surrogate or as a revelation surrogate $-\log\pi(p\mid\tau')$.
|
||||||
|
|
||||||
|
For the baseline engine reported here, we intentionally use the constant query-tax surrogate to keep the mechanism minimal:
|
||||||
|
\begin{equation}
|
||||||
|
r_t = R(p_t,\tilde q_t) - \lambda\,f(\tau_t')\,c_{\text{info}}
|
||||||
|
\end{equation}
|
||||||
|
with fixed $c_{\text{info}}>0$.
|
||||||
|
|
||||||
|
|
||||||
|
Another possible extension is to adapt the ambiguity radius online, e.g., $\epsilon(\Delta_H)$, so the Wasserstein ball changes with live divergence. We keep this as future work and retain a fixed-radius setup because Wasserstein ambiguity already handles heavy-tail and ``black swan'' behavior without absolute continuity assumptions \parencite{kuhn_wasserstein_2024}.
|
||||||
|
|
||||||
|
\subsubsection{Actor Implementation}
|
||||||
|
In our simulation, the ``follower'' is implemented as a set of Actors. Each Actor is initialized with a type $\theta$ which samples a specific demand curve $d(p; \theta)$ from the latent distribution. This formalization ensures that our DR-RL agent does not overfit to a single deterministic demand function but learns a policy robust to the distributional uncertainty defined by $\mathcal{U}_\epsilon$.
|
||||||
|
|
||||||
|
Practical implementation of browser agents is a strongly evolving field with near-weekly releases of SOTA architectures. In this thesis implementation we abstract that layer into trajectory generators learned from observed human/agent transition kernels.
|
||||||
|
|
||||||
|
|
||||||
|
As part of reward engineering, we keep a UX factor ($UX\in[0,1]$) as an auxiliary evaluation axis. In the current baseline it is not injected into the core reward; it is tracked separately to compare policy trade-offs.
|
||||||
|
|
||||||
|
\begin{figure}[ht]
|
||||||
|
\centering
|
||||||
|
\resizebox{0.5\columnwidth}{!}{%
|
||||||
|
\input{chapters/balance_figure.tex}
|
||||||
|
}
|
||||||
|
\caption{Introducing the UX index allows us to better distinguish the kind of impact different methods have and allows us to compare them on this Pareto-like scale.}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
|
We also consider taxation-like overlays for agent traffic under strategy-proof mechanism design (e.g., Vickrey-Clarke-Groves style rules). This remains an extension path and is not part of the main implementation in this thesis.
|
||||||
|
|
||||||
|
\subsubsection{Pricing Mechanism Summary}
|
||||||
|
|
||||||
|
We now present the complete pricing mechanism that integrates the behavioral separability, contamination estimation, and robust optimization components developed in the preceding sections. Algorithm~\ref{alg:phantom_loop_clean} formalizes the defensive pricing loop as a Stackelberg game where the platform (leader) sets prices and the aggregate demand (follower) responds through observed session trajectories.
|
||||||
|
|
||||||
\begin{algorithm}[t]
|
\begin{algorithm}[t]
|
||||||
|
\caption{PHANTOM defensive pricing loop}
|
||||||
|
\label{alg:phantom_loop_clean}
|
||||||
\DontPrintSemicolon
|
\DontPrintSemicolon
|
||||||
\KwIn{stepsize $\eta$, smoothing $\delta$, rank $d$}
|
\SetKwInput{Input}{Input}
|
||||||
\For{$t=1$ \KwTo $T$}{
|
\SetKwInput{Output}{Output}
|
||||||
Sample $u_t$ on unit sphere; set $x_t^\prime=x_t+\delta u_t$\;
|
|
||||||
Set $p_t \gets U x_t^\prime$ and observe $q_t, R_t(p_t)$\;
|
\Input{catalog size \(N\); action scale grid \(\mathcal{S}_{act}\); nominal contamination \(\alpha_0\); ambiguity radius \(\epsilon_\alpha\); candidate count \(K\); horizon \(T\); sessions per step \(M\); behavior kernels \(\bar T_H,\bar T_A\); event weights \(\omega\); COI penalty \(\lambda\)}
|
||||||
$x_{t+1} \gets \Pi\_{\mathcal{X}}(x_t-\eta R_t(p_t) u_t)$\;
|
\Output{trajectory \(\{(p_t,\hat Q_t,\alpha_t^*)\}_{t=0}^{T-1}\)}
|
||||||
|
\For{\(t \leftarrow 0\) \KwTo \(T-1\)}{
|
||||||
|
observe \(o_t=[\hat Q_{t-1}, p_{t-1}]\)\;
|
||||||
|
choose discrete action \(a_t \in \{1,\dots,|\mathcal{S}_{act}|\}\) from policy \(\pi\)\;
|
||||||
|
set \(p_t \leftarrow \mathrm{clip}(p_{t-1} \cdot \mathcal{S}_{act}[a_t])\)\;
|
||||||
|
|
||||||
|
define local ambiguity interval \(\mathcal{A}_{\epsilon_\alpha}(\alpha_0)=\{\alpha:\lvert\alpha-\alpha_0\rvert\le\epsilon_\alpha\}\)\;
|
||||||
|
\For{\(k \leftarrow 1\) \KwTo \(K\)}{
|
||||||
|
set \(\alpha_k \in \mathcal{A}_{\epsilon_\alpha}(\alpha_0)\) from a uniform grid\;
|
||||||
|
sample \(M\) sessions from mixture \((1-\alpha_k)\bar T_H + \alpha_k \bar T_A\)\;
|
||||||
|
compute demand proxy \(\hat Q_t^{(k)} = \sum_{m=1}^{M}\sum_j \omega(a_{m,j})\,\mathbf{1}[i_{m,j}=i]\)\;
|
||||||
|
compute \((\Delta_H^{(k)},\Delta_A^{(k)})\) and session score \(f_t^{(k)}\) from KL divergence\;
|
||||||
|
compute candidate reward \(r_t^{(k)} = R(p_t,\hat Q_t^{(k)}) - \lambda\,f_t^{(k)}\,c_{info}\)\;
|
||||||
|
}
|
||||||
|
choose \(k^* \leftarrow \arg\min_k r_t^{(k)}\), set \(\alpha_t^* \leftarrow \alpha_{k^*}\)\;
|
||||||
|
set \(\hat Q_t \leftarrow \hat Q_t^{(k^*)}\), \(r_t \leftarrow r_t^{(k^*)}\)\;
|
||||||
}
|
}
|
||||||
\caption{Online Pricing Optimization (template)}
|
|
||||||
\end{algorithm}
|
\end{algorithm}
|
||||||
|
|
||||||
|
|
||||||
|
The algorithm operates in discrete epochs indexed by $t$. At each epoch, the platform applies one discrete multiplicative price action, the environment samples a batch of sessions, and demand is recomputed from weighted events. Robustness is implemented as an inner minimization over a small local grid of contamination candidates around nominal $\alpha_0$, matching the current engine implementation. The history buffer $\mathcal{L}$ (``Limbo'' in our implementation) enforces the alternating Stackelberg structure by preserving the temporal sequence of price publications and demand observations.
|
||||||
|
|
||||||
|
%The defensive price update in Line 24 implements contamination-aware margin shrinkage: as estimated contamination $\hat{\alpha}_t$ rises, the margin $(p^{\mathrm{ref}} - c)$ is reduced by factor $\kappa\in[0,1]$, with projection $\Pi_{\mathcal{P}}$ ensuring feasibility. In subsequent experiments this heuristic rule is replaced by DR-RL policy $\pi^*$ from Eq.~\ref{eq:robust_policy}.
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
\section{Results}
|
\section{Results}
|
||||||
|
\begin{figure}[ht]
|
||||||
|
\centering
|
||||||
|
\input{chapters/figures/supra.tex}
|
||||||
|
\caption{Evolution of price distributions over experiment steps. The heatmap illustrates the density of price offerings. This is an early baseline simulation which demonstrates supra-competitive price-setting in deep learning agents such as SAC as can be clearly seen by the high density at the highest available price.}
|
||||||
|
\label{fig:supra_heatmap}
|
||||||
|
\end{figure}
|
||||||
|
|
||||||
\subsection{Behavioral Analysis}
|
\subsection{Behavioral Analysis}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,15 @@
|
|||||||
\section{Discussion}
|
\section{Discussion}
|
||||||
|
|
||||||
|
\subsection{Transition to Agentic Market Microstructure}
|
||||||
|
|
||||||
|
Our analysis of the interaction dynamics between the platform and non-human actors suggests that the current static pricing models are insufficient for an agent-mediated economy. If we assume a transition toward a direct revelation mechanism, where actors must reveal their true valuation of a good through bidding dynamics, we inevitably introduce significant stochasticity into the pricing system. Unlike traditional e-commerce where prices are relatively sticky, such a mechanism implies a high volatility characteristic of financial equity markets (without the fungability however).
|
||||||
|
|
||||||
|
However, ecommerce commodities differ fundamentally from financial securities: they possess a hard floor defined by unit economics and reservation prices. The market might react enthusiastically to an iPhone priced at \$1, such a transaction is not permissible. The platform must establish an initial valuation anchor ($P_{0}$) defined by the marginal cost plus a target margin, around which the market price is permitted to fluctuate. We propose the introduction of GenAI Agents as Institutional Market Makers.
|
||||||
|
|
||||||
|
This is also under the assumption of expected transactional capabilities being given to AI Agents.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
\subsection{Risk Assessment and Limitations}
|
\subsection{Risk Assessment and Limitations}
|
||||||
|
|
||||||
Acknowledge risks and constraints and data sizes.
|
Acknowledge risks and constraints and data sizes.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
\section{Conclusion}
|
\section{Conclusion}
|
||||||
|
|
||||||
\subsection{Summary of contributions }
|
\subsection{Summary of contributions}
|
||||||
Restate the thesis and key findings with validation of research objectives.
|
Restate the thesis and key findings with validation of research objectives.
|
||||||
|
|
||||||
\subsection{Future Works and Next Steps}
|
\subsection{Future Works and Next Steps}
|
||||||
|
|||||||
38
paper/src/chapters/balance_figure.tex
Normal file
38
paper/src/chapters/balance_figure.tex
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
\begin{tikzpicture}[
|
||||||
|
% Styles for consistency
|
||||||
|
axis/.style={->, >=Stealth, line width=1.2pt, color=black!85},
|
||||||
|
curve/.style={color=black, line width=2.5pt},
|
||||||
|
point/.style={circle, fill=black, inner sep=0pt, minimum size=6pt},
|
||||||
|
label_text/.style={font=\large, align=center, color=black},
|
||||||
|
annotation_line/.style={thick, -, color=black!60}
|
||||||
|
]
|
||||||
|
|
||||||
|
% Define Radius
|
||||||
|
\def\R{5}
|
||||||
|
|
||||||
|
% Draw Axes
|
||||||
|
% Extended slightly beyond radius (\R + 1)
|
||||||
|
\draw[axis] (0,0) -- (\R+1.5,0) node[midway, below=10pt, font=\bfseries\large] {UX Index};
|
||||||
|
\draw[axis] (0,0) -- (0,\R+1.5) node[midway, left=15pt, rotate=90, font=\bfseries\large] {Performance};
|
||||||
|
|
||||||
|
% Draw Perfect 1/4 Circle
|
||||||
|
% Syntax: arc (start_angle : end_angle : radius)
|
||||||
|
\draw[curve] (0,\R) arc (90:0:\R);
|
||||||
|
|
||||||
|
% 1. Paranoid (High Performance side) -> Angle 67.5 degrees
|
||||||
|
\node[point] (p1) at (75:\R) {};
|
||||||
|
\node[label_text, above right=0.1cm and 0.1cm of p1] (l1) {Paranoid};
|
||||||
|
\draw[annotation_line] (l1) -- (p1);
|
||||||
|
|
||||||
|
% 2. Perfect Detection (Exact Middle) -> Angle 45 degrees
|
||||||
|
\node[point] (p2) at (45:\R) {};
|
||||||
|
\node[label_text, above right=0.2cm and 0.2cm of p2] (l2) {Perfect Detection};
|
||||||
|
\draw[annotation_line] (l2) -- (p2);
|
||||||
|
|
||||||
|
% 3. No Detection (High UX side) -> Angle 22.5 degrees
|
||||||
|
\node[point] (p3) at (15:\R) {};
|
||||||
|
\node[label_text, right=0.5cm of p3] (l3) {No Detection};
|
||||||
|
\draw[annotation_line] (l3) -- (p3);
|
||||||
|
|
||||||
|
\end{tikzpicture}
|
||||||
65
paper/src/chapters/feature_table.tex
Normal file
65
paper/src/chapters/feature_table.tex
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
\begin{table}[ht]
|
||||||
|
\centering
|
||||||
|
\small
|
||||||
|
\resizebox{\columnwidth}{!}{%
|
||||||
|
\begin{tabular}{p{4.5cm}p{1.5cm}p{6cm}}
|
||||||
|
\hline
|
||||||
|
\textbf{Feature} & \textbf{Type} & \textbf{Description} \\
|
||||||
|
\hline
|
||||||
|
\multicolumn{3}{l}{\textit{Session Identifiers}} \\
|
||||||
|
sessionId & object & Unique identifier for user session \\
|
||||||
|
experimentId & object & Experiment run identifier \\
|
||||||
|
\hline
|
||||||
|
\multicolumn{3}{l}{\textit{Temporal Features}} \\
|
||||||
|
session\_duration\_sec & float & Total session duration in seconds \\
|
||||||
|
avg\_time\_between\_events & float & Mean inter-event time \\
|
||||||
|
std\_time\_between\_events & float & Standard deviation of inter-event times \\
|
||||||
|
min\_time\_between\_events & float & Minimum time between consecutive events \\
|
||||||
|
session\_start\_hour & int & Hour of day when session started \\
|
||||||
|
\hline
|
||||||
|
\multicolumn{3}{l}{\textit{Interaction Metrics}} \\
|
||||||
|
total\_interactions & int & Count of all user interactions \\
|
||||||
|
total\_events & int & Total number of tracked events \\
|
||||||
|
interaction\_velocity & float & Rate of interactions per time unit \\
|
||||||
|
max\_velocity\_5min & int & Peak interaction count in any 5-minute window \\
|
||||||
|
\hline
|
||||||
|
\multicolumn{3}{l}{\textit{Navigation Behavior}} \\
|
||||||
|
unique\_pages & int & Number of distinct pages visited \\
|
||||||
|
page\_views & int & Total page view events \\
|
||||||
|
\hline
|
||||||
|
\multicolumn{3}{l}{\textit{Product Engagement}} \\
|
||||||
|
item\_views & int & Number of product detail views \\
|
||||||
|
unique\_products\_viewed & int & Count of distinct products examined \\
|
||||||
|
product\_view\_depth & int & Repeat views of same products \\
|
||||||
|
\hline
|
||||||
|
\multicolumn{3}{l}{\textit{Conversion Funnel}} \\
|
||||||
|
cart\_adds & int & Number of items added to cart \\
|
||||||
|
purchases & int & Completed transactions \\
|
||||||
|
cart\_to\_view\_ratio & float & Ratio of cart additions to item views \\
|
||||||
|
conversion\_rate & float & Purchase to view conversion \\
|
||||||
|
\hline
|
||||||
|
\multicolumn{3}{l}{\textit{Interaction Quality}} \\
|
||||||
|
hover\_events & int & Mouse hover event count \\
|
||||||
|
hover\_intensity & float & Hover events per interaction \\
|
||||||
|
\hline
|
||||||
|
\multicolumn{3}{l}{\textit{Price Behavior}} \\
|
||||||
|
avg\_price\_seen & float & Mean price across viewed products \\
|
||||||
|
min\_price\_seen & float & Lowest price encountered \\
|
||||||
|
max\_price\_seen & float & Highest price encountered \\
|
||||||
|
price\_range & float & Difference between max and min prices seen \\
|
||||||
|
\hline
|
||||||
|
\multicolumn{3}{l}{\textit{Technical Fingerprinting}} \\
|
||||||
|
is\_headless & bool & Headless browser detection flag \\
|
||||||
|
is\_automation & bool & Automation framework detection flag \\
|
||||||
|
browser\_family & object & Browser type classification \\
|
||||||
|
\hline
|
||||||
|
\multicolumn{3}{l}{\textit{Experimental Labels}} \\
|
||||||
|
is\_agent & bool & Ground truth agent classification \\
|
||||||
|
xp\_human\_only & bool & Human-only experiment indicator \\
|
||||||
|
xp\_market\_mode & object & Market context (hotel/airline) \\
|
||||||
|
\hline
|
||||||
|
\end{tabular}%
|
||||||
|
}
|
||||||
|
\caption{Feature matrix schema for session-level behavioral classification (32 features total).}
|
||||||
|
\label{tab:features}
|
||||||
|
\end{table}
|
||||||
131
paper/src/chapters/figures/process_supra.py
Normal file
131
paper/src/chapters/figures/process_supra.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import pandas as pd
|
||||||
|
import json
|
||||||
|
import numpy as np
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def process_supra(input_file, output_file):
|
||||||
|
print(f"Processing {input_file} -> {output_file}")
|
||||||
|
|
||||||
|
# Read the CSV
|
||||||
|
try:
|
||||||
|
# The CSV has a weird format: "Step","giddy-deluge-6 - distributions/prices"
|
||||||
|
# The header is on line 1.
|
||||||
|
# Let's verify the file content format first effectively.
|
||||||
|
# The previous read showed standard CSV with quoted fields.
|
||||||
|
df = pd.read_csv(input_file, quotechar='"', skipinitialspace=True)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading CSV: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Prepare for re-binning
|
||||||
|
# We need a common set of bins to plot a heatmap (surface)
|
||||||
|
# First, let's collect all data to determine range
|
||||||
|
all_min = float("inf")
|
||||||
|
all_max = float("-inf")
|
||||||
|
|
||||||
|
parsed_data = []
|
||||||
|
|
||||||
|
# The column names might be dynamic, so let's rely on indices
|
||||||
|
# Column 0: Step
|
||||||
|
# Column 1: JSON blob
|
||||||
|
|
||||||
|
for index, row in df.iterrows():
|
||||||
|
try:
|
||||||
|
step = int(row.iloc[0])
|
||||||
|
json_str = row.iloc[1]
|
||||||
|
|
||||||
|
# Cleaning potential double quotes issue if pandas didn't catch it perfect
|
||||||
|
# but pandas read_csv usually handles standard CSV escaping well.
|
||||||
|
|
||||||
|
data = json.loads(json_str)
|
||||||
|
|
||||||
|
bins = np.array(data["bins"])
|
||||||
|
values = np.array(data["values"])
|
||||||
|
|
||||||
|
# Update global range
|
||||||
|
if bins.min() < all_min:
|
||||||
|
all_min = bins.min()
|
||||||
|
if bins.max() > all_max:
|
||||||
|
all_max = bins.max()
|
||||||
|
|
||||||
|
parsed_data.append({"step": step, "bins": bins, "values": values})
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Skipping row {index} due to error: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not parsed_data:
|
||||||
|
print("No data parsed.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Found {len(parsed_data)} steps. Range: {all_min} to {all_max}")
|
||||||
|
|
||||||
|
# Define common grid
|
||||||
|
# Y-axis (Price)
|
||||||
|
# Using 100 bins for resolution
|
||||||
|
y_bins_edges = np.linspace(all_min, all_max, 101)
|
||||||
|
y_bin_centers = (y_bins_edges[:-1] + y_bins_edges[1:]) / 2
|
||||||
|
|
||||||
|
# Open output file
|
||||||
|
with open(output_file, "w") as f:
|
||||||
|
# PGFPlots 3D format often prefers no header or a specific header.
|
||||||
|
# We will use named columns.
|
||||||
|
f.write("step,price,density\n")
|
||||||
|
|
||||||
|
# Sort by step to ensure correct mesh ordering
|
||||||
|
parsed_data.sort(key=lambda x: x["step"])
|
||||||
|
|
||||||
|
for item in parsed_data:
|
||||||
|
step = item["step"]
|
||||||
|
original_bins = item["bins"]
|
||||||
|
original_values = item["values"]
|
||||||
|
|
||||||
|
# Re-binning logic
|
||||||
|
current_new_hist = np.zeros(len(y_bin_centers))
|
||||||
|
|
||||||
|
for i, (new_start, new_end) in enumerate(
|
||||||
|
zip(y_bins_edges[:-1], y_bins_edges[1:])
|
||||||
|
):
|
||||||
|
val = 0.0
|
||||||
|
# This inner loop is slightly inefficient O(N*M) but N~3000, M~100 -> 300k ops, totally fine.
|
||||||
|
for j in range(len(original_values)):
|
||||||
|
b_start = original_bins[j]
|
||||||
|
# Handle cases where values array might be 1 shorter than bins (histogram edges vs centers)
|
||||||
|
# The provided JSON has "bins" array larger than "values" by 1 usually for edges.
|
||||||
|
if j + 1 >= len(original_bins):
|
||||||
|
break
|
||||||
|
|
||||||
|
b_end = original_bins[j + 1]
|
||||||
|
b_width = b_end - b_start
|
||||||
|
|
||||||
|
if b_width <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Calculate overlap
|
||||||
|
overlap_start = max(new_start, b_start)
|
||||||
|
overlap_end = min(new_end, b_end)
|
||||||
|
overlap = max(0, overlap_end - overlap_start)
|
||||||
|
|
||||||
|
if overlap > 0:
|
||||||
|
# Add proportional count
|
||||||
|
val += original_values[j] * (overlap / b_width)
|
||||||
|
|
||||||
|
current_new_hist[i] = val
|
||||||
|
|
||||||
|
# Write row to file for this step
|
||||||
|
for price, density in zip(y_bin_centers, current_new_hist):
|
||||||
|
# PGFPlots expects x y z
|
||||||
|
f.write(f"{step},{price},{density}\n")
|
||||||
|
|
||||||
|
# Add a blank line for PGFPlots matrix format (essential for 'mesh' or 'surf')
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# Resolve relative paths relative to where script is run, or use absolute
|
||||||
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
input_path = os.path.join(base_dir, "supra.csv")
|
||||||
|
output_path = os.path.join(base_dir, "supra_data.csv")
|
||||||
|
|
||||||
|
process_supra(input_path, output_path)
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user