Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# log file
*/logs/*
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "BusinessMonitor",
"version": "0.1.0+96",
"version": "0.1.0+100",
"private": true,
"type": "module",
"scripts": {
Expand Down Expand Up @@ -38,9 +38,11 @@
"@prisma/client": "^6.13.0",
"@types/supertest": "^6.0.0",
"autoprefixer": "^10.4.21",
"csv-parse": "^6.1.0",
"i18next": "^25.3.2",
"i18next-resources-to-backend": "^1.2.1",
"i18nexus-cli": "^3.7.0",
"iconv-lite": "^0.7.0",
"json2csv": "^6.0.0-alpha.2",
"next": "15.4.4",
"next-i18n-router": "^5.5.3",
Expand All @@ -53,7 +55,7 @@
"secp256k1": "^5.0.1",
"supertest": "^7.0.0",
"tailwindcss": "^3.4.17",
"zod": "^4.0.15"
"zod": "^4.1.5"
},
"devDependencies": {
"@asteasolutions/zod-to-openapi": "^8.1.0",
Expand Down Expand Up @@ -81,11 +83,11 @@
"lint-staged": "^16.1.2",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"prisma": "^6.12.0",
"prisma": "^6.15.0",
"prisma-erd-generator": "^2.0.4",
"ts-jest": "^29.4.1",
"ts-node": "^10.9.2",
"tsx": "^4.20.3",
"tsx": "^4.20.5",
"typescript": "^5.9.2"
}
}
53 changes: 53 additions & 0 deletions prisma/migrations/007_add_market_daily_tables/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
-- CreateTable
CREATE TABLE "public"."market_daily_price" (
"id" SERIAL NOT NULL,
"market" TEXT NOT NULL,
"date" TIMESTAMP(3) NOT NULL,
"symbol" TEXT NOT NULL,
"name" TEXT,
"trade_volume" BIGINT,
"trade_value" DECIMAL(30,0),
"trade_count" INTEGER,
"open_price" DECIMAL(20,4),
"high_price" DECIMAL(20,4),
"low_price" DECIMAL(20,4),
"close_price" DECIMAL(20,4),
"change_sign" TEXT,
"change_amount" DECIMAL(20,4),
"final_bid_price" DECIMAL(20,4),
"final_bid_volume" BIGINT,
"final_ask_price" DECIMAL(20,4),
"final_ask_volume" BIGINT,
"pe_ratio" DECIMAL(10,2),
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,

CONSTRAINT "market_daily_price_pkey" PRIMARY KEY ("id")
);

-- CreateTable
CREATE TABLE "public"."market_daily_summary" (
"id" SERIAL NOT NULL,
"market" TEXT NOT NULL,
"date" TIMESTAMP(3) NOT NULL,
"category" TEXT NOT NULL,
"trade_value" DECIMAL(30,0),
"trade_volume" BIGINT,
"trade_count" INTEGER,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,

CONSTRAINT "market_daily_summary_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE INDEX "market_daily_price_market_date_symbol_idx" ON "public"."market_daily_price"("market", "date", "symbol");

-- CreateIndex
CREATE UNIQUE INDEX "market_daily_price_market_date_symbol_key" ON "public"."market_daily_price"("market", "date", "symbol");

-- CreateIndex
CREATE INDEX "market_daily_summary_market_date_category_idx" ON "public"."market_daily_summary"("market", "date", "category");

-- CreateIndex
CREATE UNIQUE INDEX "market_daily_summary_market_date_category_key" ON "public"."market_daily_summary"("market", "date", "category");
48 changes: 48 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,51 @@ model PoliticalActivity {
@@unique([companyId, event, date, recipient, type], map: "uniq_pc_company_event_date_recipient_type") // Info: (20250827 - Tzuhan) 避免重複
@@map("political_activity")
}

// Info: (20250903 - Tzuhan) 證交所每日「逐檔」行情(含 ETF / 權證等)
model MarketDailyPrice {
id Int @id @default(autoincrement())
market String // Info: (20250903 - Tzuhan) 例如 "TWSE"
date DateTime // Info: (20250903 - Tzuhan) 以 UTC 00:00 儲存該交易日
symbol String // Info: (20250903 - Tzuhan) 證券代號,例如 0050、2330、0302P...
name String? // Info: (20250903 - Tzuhan) 證券名稱
tradeVolume BigInt? @map("trade_volume") // Info: (20250903 - Tzuhan) 成交股數
tradeValue Decimal? @db.Decimal(30,0) @map("trade_value") // Info: (20250903 - Tzuhan) 成交金額(元)
tradeCount Int? @map("trade_count") // Info: (20250903 - Tzuhan) 成交筆數
openPrice Decimal? @db.Decimal(20,4) @map("open_price")
highPrice Decimal? @db.Decimal(20,4) @map("high_price")
lowPrice Decimal? @db.Decimal(20,4) @map("low_price")
closePrice Decimal? @db.Decimal(20,4) @map("close_price")
changeSign String? @map("change_sign") // Info: (20250903 - Tzuhan) '+' | '-' | 'X'(無漲跌)
changeAmount Decimal? @db.Decimal(20,4) @map("change_amount")
finalBidPrice Decimal? @db.Decimal(20,4) @map("final_bid_price")
finalBidVolume BigInt? @map("final_bid_volume")
finalAskPrice Decimal? @db.Decimal(20,4) @map("final_ask_price")
finalAskVolume BigInt? @map("final_ask_volume")
peRatio Decimal? @db.Decimal(10,2) @map("pe_ratio")

createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

@@unique([market, date, symbol], name: "market_date_symbol_unique")
@@index([market, date, symbol])
@@map("market_daily_price")
}

// Info: (20250903 - Tzuhan) 證交所每日「分類彙總」:一般股票、ETF、權證… 的 金額/股數/筆數
model MarketDailySummary {
id Int @id @default(autoincrement())
market String
date DateTime
category String // Info: (20250903 - Tzuhan) 例如 "一般股票"、"ETF"、"台灣存託憑證"...
tradeValue Decimal? @db.Decimal(30,0) @map("trade_value")
tradeVolume BigInt? @map("trade_volume")
tradeCount Int? @map("trade_count")

createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")

@@unique([market, date, category], name: "market_date_category_unique")
@@index([market, date, category])
@@map("market_daily_summary")
}
76 changes: 76 additions & 0 deletions scripts/audit_files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import fs from 'node:fs';
import path from 'node:path';
import iconv from 'iconv-lite';

function parseYmd(s: string): Date {
const y = Number(s.slice(0, 4)),
m = Number(s.slice(4, 6)),
d = Number(s.slice(6, 8));
return new Date(Date.UTC(y, m - 1, d));
}
function fmtYmd(d: Date): string {
const y = d.getUTCFullYear(),
m = `${d.getUTCMonth() + 1}`.padStart(2, '0'),
dd = `${d.getUTCDate()}`.padStart(2, '0');
return `${y}${m}${dd}`;
}
function* rangeDays(start: Date, end: Date) {
for (let t = new Date(start); t <= end; t.setUTCDate(t.getUTCDate() + 1)) yield new Date(t);
}

const dir = process.argv[2];
const startYmd = process.argv[3];
const endYmd = process.argv[4];
if (!dir || !startYmd || !endYmd) {
console.error('用法:npx tsx scripts/audit_files.ts <資料夾> <起始YYYYMMDD> <結束YYYYMMDD>');
process.exit(1);
}

const start = parseYmd(startYmd);
const end = parseYmd(endYmd);

// Info: (20250905 - Tzuhan) 遞迴收集所有 YYYYMMDD.csv
const all: Record<string, string> = {};
function walk(p: string) {
for (const name of fs.readdirSync(p)) {
const fp = path.join(p, name);
const st = fs.statSync(fp);
if (st.isDirectory()) walk(fp);
else if (/^\d{8}\.csv$/i.test(name)) all[name.slice(0, 8)] = fp;
}
}
walk(dir);

// Info: (20250905 - Tzuhan) 寫 CSV
fs.writeFileSync(
'files_audit.csv',
'date,weekday,exists,path,size,header_ok,has_section,notes\n',
'utf8'
);

const missing: string[] = [];
for (const d of rangeDays(start, end)) {
const ymd = fmtYmd(d);
const wk = d.getUTCDay(); // Info: (20250905 - Tzuhan) 0 Sun ~ 6 Sat
const fp = all[ymd];
if (!fp) {
const tag = wk === 0 || wk === 6 ? 'weekend' : 'weekday';
missing.push(`${ymd},${tag}`);
fs.appendFileSync('files_audit.csv', `${ymd},${wk},false,,,false,false,missing\n`);
continue;
}

const raw = fs.readFileSync(fp);
const txt = iconv.decode(raw, 'big5').replace(/\r\n?/g, '\n');
const hasSection = txt.includes('每日收盤行情');
const headerOk = txt.includes('證券代號') && txt.includes('成交金額') && txt.includes('收盤價');
const size = fs.statSync(fp).size;

fs.appendFileSync(
'files_audit.csv',
`${ymd},${wk},true,${JSON.stringify(fp)},${size},${headerOk},${hasSection},\n`
);
}

fs.writeFileSync('missing_dates.csv', `date,tag\n${missing.join('\n')}\n`, 'utf8');
console.log('完成檢核:files_audit.csv、missing_dates.csv 已產生。');
Loading