Prisma schema初版・画面設計・docker-compose初版
はじめに(今回やること)
以前、PHP Laravelで作成し、現在も個人利用しているFXのトレード履歴管理Webアプリがあります。
一応実用はできているのですが、改善したい点や不具合が残ったままになっていました。
そこで今回、アプリを TypeScriptベースで再構築 することにしました。
新たに 「FXトレードジャーナル」 として作り直し、その過程をブログとして記録していきます。
本記事では、フェーズ1(設計・土台づくり) の内容をまとめます。
全体構想(今後の予定)
フェーズ1:設計と土台づくり(目的:設計を固めて開発を始められる状態にする)
1.テーブル設計・画面設計(今回)
2.API設計
3.ログイン画面 / ダッシュボード骨組み / docker-compose初版
4.Prismaマイグレーション & 初期データ投入
5.共通レイアウト・メニュー作成
フェーズ2:取引一覧・詳細の実装
フェーズ3:可視化・実用性アップ
既存システムの構成と再構築方針
現状は、以下の構成で運用しています。
- Laravel版Webアプリ(取引履歴表示)
- MT4インジケータ(取引履歴ファイルの定期出力)
- WinSCP + PowerShell + Windowsタスクスケジューラ(サーバーへ転送)
この構成自体は責務分離できていて悪くないため、新版でも基本的にはこの分解を活かす方針にしました。
再構築の方針(初期)
- ②MT4インジケータ:当面そのまま利用
- ③WinSCP + PowerShell:当面そのまま利用
- ①Webアプリ部分:TypeScript + Docker で再構築(最優先)
まずは Web/API/DB/取込土台 を作り、後から必要であれば転送部分(PowerShell/WinSCP)を置き換える段階移行にします。
今回の技術選定
今回の再構築では、以下の構成を採用しました。
- Next.js(TypeScript)
- Prisma
- PostgreSQL
- Docker Compose
この構成にした理由
- TypeScript を学習しながら、画面/API/バッチを同一言語で揃えやすい
- Next.js は UI と API を一体で作りやすく、ポートフォリオにも向いている
- Prisma は型安全にDBを扱いやすく、設計の見通しが良い
- Docker Compose でローカル再現性を高めやすい
Prisma schema(初版)
初版のポイントは以下です。
- Laravel版との互換性を意識し、取引の業務ユニークキー(broker_id + account_no + ticket_no)は維持
- Prisma/TypeScript側で扱いやすいように内部ID(cuid)も付与
- 既存の temp_equity は「上書き」ではなく、履歴型の EquitySnapshot に変更
- 運用状況を追いやすくするため ImportJob / ImportJobFile を追加
まずはこのスキーマでテーブルを作り、フェーズ2でAPI実装を進める予定です。
schema.prisma
generator client {
provider = “prisma-client-js”
}
datasource db {
provider = “postgresql”
url = env(“DATABASE_URL”)
}
enum TradeSide {
BUY
SELL
OTHER
}
enum TradeStatus {
OPEN
CLOSED
}
enum ImportJobType {
TRADE_CSV
TRADE_IMAGE
EQUITY_SNAPSHOT
}
enum ImportJobStatus {
RUNNING
SUCCESS
PARTIAL
FAILED
}
enum BaselineType {
DAILY_0900
}
model User {
id String @id @default(cuid())
email String @unique
passwordHash String
name String?
role String? // 将来拡張用
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
tradeNotes TradeNote[]
}
model Broker {
id Int @id @default(autoincrement())
name String @unique
code String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
accounts Account[]
trades Trade[]
}
model Account {
id String @id @default(cuid())
userId String
brokerId Int
accountNo String
currency String @default(“JPY”)
note String?
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
broker Broker @relation(fields: [brokerId], references: [id], onDelete: Restrict)
snapshots EquitySnapshot[]
@@unique([userId, brokerId, accountNo], map: “uq_account_user_broker_accountno”)
@@index([brokerId, accountNo], map: “idx_account_broker_accountno”)
}
model Trade {
id String @id @default(cuid())
brokerId Int
accountNo String
ticketNo String
symbol String?
side TradeSide @default(OTHER)
status TradeStatus @default(CLOSED)
orderTypeRaw String? // MT4 raw value / stringを保持したい場合
lots Decimal? @db.Decimal(12, 4)
openPrice Decimal? @db.Decimal(18, 8)
closePrice Decimal? @db.Decimal(18, 8)
sl Decimal? @db.Decimal(18, 8)
tp Decimal? @db.Decimal(18, 8)
orderTime DateTime?
closeTime DateTime?
commission Decimal? @db.Decimal(18, 4)
swap Decimal? @db.Decimal(18, 4)
profit Decimal? @db.Decimal(18, 4)
mt4Comment String?
journalComment String? // アプリ側コメント(簡易版。詳細メモはtrade_notesへ)
importedAt DateTime @default(now())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
broker Broker @relation(fields: [brokerId], references: [id], onDelete: Restrict)
images TradeImage[]
notes TradeNote[]
tradeTags TradeTag[]
@@unique([brokerId, accountNo, ticketNo], map: “uq_trade_broker_account_ticket”)
@@index([brokerId, accountNo, orderTime(sort: Desc)], map: “idx_trade_broker_account_ordertime”)
@@index([brokerId, accountNo, closeTime(sort: Desc)], map: “idx_trade_broker_account_closetime”)
@@index([symbol, closeTime(sort: Desc)], map: “idx_trade_symbol_closetime”)
@@index([status, closeTime(sort: Desc)], map: “idx_trade_status_closetime”)
@@index([side, closeTime(sort: Desc)], map: “idx_trade_side_closetime”)
}
model TradeImage {
id String @id @default(cuid())
tradeId String?
brokerId Int?
accountNo String?
ticketNo String?
storageProvider String @default(“local”) // local / s3 etc.
disk String? // local disk名など
path String
mime String?
size Int?
capturedAt DateTime?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
trade Trade? @relation(fields: [tradeId], references: [id], onDelete: SetNull)
@@index([tradeId], map: “idx_trade_image_tradeid”)
@@index([brokerId, accountNo, ticketNo], map: “idx_trade_image_broker_account_ticket”)
}
model TradeNote {
id String @id @default(cuid())
tradeId String
userId String
noteText String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
trade Trade @relation(fields: [tradeId], references: [id], onDelete: Cascade)
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([tradeId], map: “idx_trade_note_tradeid”)
@@index([userId], map: “idx_trade_note_userid”)
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
color String?
createdAt DateTime @default(now())
tradeTags TradeTag[]
}
model TradeTag {
tradeId String
tagId Int
createdAt DateTime @default(now())
trade Trade @relation(fields: [tradeId], references: [id], onDelete: Cascade)
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
@@id([tradeId, tagId])
@@index([tagId], map: “idx_trade_tag_tagid”)
}
model EquitySnapshot {
id String @id @default(cuid())
accountId String
snapshotAt DateTime
snapshotDate DateTime // JST日付(00:00)などで保存
baselineType BaselineType
amount Decimal @db.Decimal(18, 4)
createdAt DateTime @default(now())
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
@@unique([accountId, baselineType, snapshotDate], map: “uq_snapshot_account_baseline_date”)
@@index([snapshotAt(sort: Desc)], map: “idx_snapshot_snapshotat”)
}
model ImportJob {
id String @id @default(cuid())
jobType ImportJobType
status ImportJobStatus
startedAt DateTime @default(now())
finishedAt DateTime?
targetPath String?
processedFiles Int @default(0)
importedCount Int @default(0)
updatedCount Int @default(0)
skippedCount Int @default(0)
failedCount Int @default(0)
message String?
createdBy String? // system / manual
createdAt DateTime @default(now())
files ImportJobFile[]
}
model ImportJobFile {
id String @id @default(cuid())
importJobId String
fileName String
filePath String?
status ImportJobStatus
message String?
createdAt DateTime @default(now())
importJob ImportJob @relation(fields: [importJobId], references: [id], onDelete: Cascade)
@@index([importJobId], map: “idx_import_job_file_jobid”)
}
画面一覧と項目定義(初版)
フェーズ1時点では、いきなり全画面を作るのではなく、先に必要な画面を整理しました。
フェーズ1で対象にする画面
- ログイン画面
- ダッシュボード画面
- 取引一覧画面
- 取引詳細画面
- 損益カレンダー画面
- インポート履歴画面(簡易)
特に今回は「後からAPI設計しやすいように、どの画面で何を表示/入力するか」を先に明確にしています。
docker-compose初版
今回はまず「開発環境を早く立ち上げる」ことを優先して、シンプルな構成にしました。
- db(PostgreSQL)
- web(Next.js)
- worker(取込バッチ用)
初版での割り切り
- cronコンテナはまだ用意しない(手動実行で確認)
- web / worker は起動時に npm install 実行(開発用の簡易構成)
- 画像保存は local volume(将来S3/MinIO対応を検討)
既存の WinSCP 転送先を data/inbox / data/img_inbox に合わせれば、既存運用を大きく変えずに移行を進められる想定です。
docker-compose.yml
version: “3.9”
services:
db:
image: postgres:16-alpine
container_name: fxjournal-db
restart: unless-stopped
environment:
POSTGRES_DB: fxjournal
POSTGRES_USER: fxuser
POSTGRES_PASSWORD: fxpass
TZ: Asia/Tokyo
ports:
– “5432:5432”
volumes:
– pgdata:/var/lib/postgresql/data
healthcheck:
test: [“CMD-SHELL”, “pg_isready -U fxuser -d fxjournal”]
interval: 10s
timeout: 5s
retries: 5
web:
build:
context: .
dockerfile: docker/Dockerfile
container_name: fxjournal-web
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
NODE_ENV: development
TZ: Asia/Tokyo
DATABASE_URL: postgresql://fxuser:fxpass@db:5432/fxjournal?schema=public
APP_TIMEZONE: Asia/Tokyo
MT4_TIMEZONE_OFFSET_HOURS: “2” # 例。運用に合わせて調整
IMPORT_CSV_DIR: /work/data/inbox
IMPORT_IMG_DIR: /work/data/img_inbox
STORAGE_DIR: /work/data/storage
ports:
– “3000:3000”
volumes:
– ./:/work
– /work/node_modules
– /work/.next
– ./data:/work/data
working_dir: /work
command: sh -c “npm install && npx prisma generate && npm run dev”
worker:
build:
context: .
dockerfile: docker/worker.Dockerfile
container_name: fxjournal-worker
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
NODE_ENV: development
TZ: Asia/Tokyo
DATABASE_URL: postgresql://fxuser:fxpass@db:5432/fxjournal?schema=public
APP_TIMEZONE: Asia/Tokyo
MT4_TIMEZONE_OFFSET_HOURS: “2”
IMPORT_CSV_DIR: /work/data/inbox
IMPORT_IMG_DIR: /work/data/img_inbox
STORAGE_DIR: /work/data/storage
volumes:
– ./:/work
– /work/node_modules
– ./data:/work/data
working_dir: /work
# 初版は常駐だけして、手動で docker compose exec worker … で実行
command: sh -c “npm install && npx prisma generate && tail -f /dev/null”
volumes:
pgdata:
まとめ
今回はフェーズ1として、FXトレードジャーナル再構築の土台になる以下を整理しました。
- Prisma schema(初版)
- 画面一覧と項目定義
- docker-compose初版
いきなり実装に入ると後から修正コストが大きくなるため、まずは設計と土台を先に固める形にしました。
既存の Laravel版/MT4/WinSCP の運用を活かしながら段階移行できる形を意識しています。
次回は、今回の画面定義をもとに API設計(一覧取得・詳細取得・ダッシュボード集計・取込履歴)を整理していく予定です。


コメント