Compare commits
12 Commits
e0b6eab7cf
...
1f83b6abf9
Author | SHA1 | Date | |
---|---|---|---|
1f83b6abf9 | |||
10324d5799 | |||
ad7ab6db86 | |||
ca9dd95461 | |||
fae9cc8154 | |||
e6d7020fc8 | |||
32c1458a9c | |||
22fad337ae | |||
454850c6b3 | |||
ea611f0cdc | |||
d31855785d | |||
f089a04ef4 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -10,3 +10,6 @@ stock.db
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# Fresh build directory
|
||||
_fresh/
|
||||
|
@ -1,3 +1,5 @@
|
||||
# Stock
|
||||
|
||||
[![Made with Fresh](https://fresh.deno.dev/fresh-badge.svg)](https://fresh.deno.dev)
|
||||
|
||||
주식 데이터 수집 및 선별하는 파이썬 코드입니다.
|
||||
|
24
app.py
24
app.py
@ -1,24 +0,0 @@
|
||||
import flask
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description="Stock web server")
|
||||
parser.add_argument("--port", type=int, default=12001, help="port number")
|
||||
parser.add_argument("--host", type=str, default="0.0.0.0", help="host address")
|
||||
parser.add_argument("--debug", action="store_true", help="debug mode")
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
|
||||
|
||||
@app.route("/dist/<m>")
|
||||
def distServe(m:str):
|
||||
return flask.send_from_directory("dist", m)
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
import pages
|
||||
return flask.render_template("index.html", pages = pages.GenLists)
|
||||
|
||||
if __name__ == '__main__':
|
||||
args = parser.parse_args()
|
||||
app.run(host=args.host, port=args.port, debug=args.debug)
|
||||
|
11
db/db.ts
Normal file
11
db/db.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { Kysely, ParseJSONResultsPlugin } from "kysely";
|
||||
import { DB as Sqlite } from "sqlite";
|
||||
import { DenoSqliteDialect } from "./deno-sqlite-dialect.ts";
|
||||
import { Database } from "./type.ts";
|
||||
|
||||
export const db = new Kysely<Database>({
|
||||
dialect: new DenoSqliteDialect({
|
||||
database: new Sqlite("stock.db")
|
||||
}),
|
||||
plugins: [new ParseJSONResultsPlugin()]
|
||||
});
|
27
db/deno-sqlite-dialect-config.ts
Normal file
27
db/deno-sqlite-dialect-config.ts
Normal file
@ -0,0 +1,27 @@
|
||||
/// The MIT License (MIT)
|
||||
/// Copyright (c) 2023 Alex Gleason
|
||||
/// Copyright (c) 2022 Sami Koskimäki
|
||||
/// https://gitlab.com/soapbox-pub/kysely-deno-sqlite
|
||||
|
||||
import type { SqliteDialectConfig } from 'kysely';
|
||||
|
||||
/** Type compatible with both [dyedgreen/deno-sqlite](https://github.com/dyedgreen/deno-sqlite) and [denodrivers/sqlite3](https://github.com/denodrivers/sqlite3). */
|
||||
type DenoSqlite =
|
||||
& {
|
||||
close(): void;
|
||||
changes: number;
|
||||
lastInsertRowId: number;
|
||||
}
|
||||
& ({
|
||||
queryEntries(sql: string, params: any): unknown[];
|
||||
} | {
|
||||
prepare(sql: string): {
|
||||
all(...params: any): unknown[];
|
||||
};
|
||||
});
|
||||
|
||||
interface DenoSqliteDialectConfig extends Omit<SqliteDialectConfig, 'database'> {
|
||||
database: DenoSqlite | (() => Promise<DenoSqlite>);
|
||||
}
|
||||
|
||||
export type { DenoSqlite, DenoSqliteDialectConfig };
|
49
db/deno-sqlite-dialect.ts
Normal file
49
db/deno-sqlite-dialect.ts
Normal file
@ -0,0 +1,49 @@
|
||||
/// The MIT License (MIT)
|
||||
/// Copyright (c) 2023 Alex Gleason
|
||||
/// Copyright (c) 2022 Sami Koskimäki
|
||||
/// https://gitlab.com/soapbox-pub/kysely-deno-sqlite
|
||||
|
||||
import {
|
||||
type DatabaseIntrospector,
|
||||
type Dialect,
|
||||
type DialectAdapter,
|
||||
type Driver,
|
||||
Kysely,
|
||||
type QueryCompiler,
|
||||
SqliteAdapter,
|
||||
SqliteIntrospector,
|
||||
SqliteQueryCompiler,
|
||||
} from 'kysely';
|
||||
|
||||
import { DenoSqliteDriver } from './kysely-sqlite-driver.ts';
|
||||
|
||||
import type { DenoSqliteDialectConfig } from './deno-sqlite-dialect-config.ts';
|
||||
|
||||
class DenoSqliteDialect implements Dialect {
|
||||
readonly #config: DenoSqliteDialectConfig;
|
||||
|
||||
constructor(config: DenoSqliteDialectConfig) {
|
||||
this.#config = Object.freeze({ ...config });
|
||||
}
|
||||
|
||||
createDriver(): Driver {
|
||||
return new DenoSqliteDriver(this.#config);
|
||||
}
|
||||
|
||||
createQueryCompiler(): QueryCompiler {
|
||||
return new SqliteQueryCompiler();
|
||||
}
|
||||
|
||||
createAdapter(): DialectAdapter {
|
||||
return new SqliteAdapter();
|
||||
}
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
createIntrospector(db: Kysely<any>): DatabaseIntrospector {
|
||||
return new SqliteIntrospector(db);
|
||||
}
|
||||
}
|
||||
|
||||
export { DenoSqliteDialect };
|
||||
|
||||
|
112
db/kysely-sqlite-driver.ts
Normal file
112
db/kysely-sqlite-driver.ts
Normal file
@ -0,0 +1,112 @@
|
||||
/// The MIT License (MIT)
|
||||
/// Copyright (c) 2023 Alex Gleason
|
||||
/// Copyright (c) 2022 Sami Koskimäki
|
||||
/// https://gitlab.com/soapbox-pub/kysely-deno-sqlite
|
||||
|
||||
import { CompiledQuery, type DatabaseConnection, type Driver, type QueryResult } from 'kysely';
|
||||
|
||||
import type { DenoSqlite, DenoSqliteDialectConfig } from './deno-sqlite-dialect-config.ts';
|
||||
|
||||
class DenoSqliteDriver implements Driver {
|
||||
readonly #config: DenoSqliteDialectConfig;
|
||||
readonly #connectionMutex = new ConnectionMutex();
|
||||
|
||||
#db?: DenoSqlite;
|
||||
#connection?: DatabaseConnection;
|
||||
|
||||
constructor(config: DenoSqliteDialectConfig) {
|
||||
this.#config = Object.freeze({ ...config });
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
this.#db = typeof this.#config.database === 'function' ? await this.#config.database() : this.#config.database;
|
||||
|
||||
this.#connection = new DenoSqliteConnection(this.#db);
|
||||
|
||||
if (this.#config.onCreateConnection) {
|
||||
await this.#config.onCreateConnection(this.#connection);
|
||||
}
|
||||
}
|
||||
|
||||
async acquireConnection(): Promise<DatabaseConnection> {
|
||||
// SQLite only has one single connection. We use a mutex here to wait
|
||||
// until the single connection has been released.
|
||||
await this.#connectionMutex.lock();
|
||||
return this.#connection!;
|
||||
}
|
||||
|
||||
async beginTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw('begin'));
|
||||
}
|
||||
|
||||
async commitTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw('commit'));
|
||||
}
|
||||
|
||||
async rollbackTransaction(connection: DatabaseConnection): Promise<void> {
|
||||
await connection.executeQuery(CompiledQuery.raw('rollback'));
|
||||
}
|
||||
|
||||
// deno-lint-ignore require-await
|
||||
async releaseConnection(): Promise<void> {
|
||||
this.#connectionMutex.unlock();
|
||||
}
|
||||
|
||||
// deno-lint-ignore require-await
|
||||
async destroy(): Promise<void> {
|
||||
this.#db?.close();
|
||||
}
|
||||
}
|
||||
|
||||
class DenoSqliteConnection implements DatabaseConnection {
|
||||
readonly #db: DenoSqlite;
|
||||
|
||||
constructor(db: DenoSqlite) {
|
||||
this.#db = db;
|
||||
}
|
||||
|
||||
executeQuery<O>({ sql, parameters }: CompiledQuery): Promise<QueryResult<O>> {
|
||||
const rows = 'queryEntries' in this.#db
|
||||
? this.#db.queryEntries(sql, parameters)
|
||||
: this.#db.prepare(sql).all(...parameters);
|
||||
|
||||
const { changes, lastInsertRowId } = this.#db;
|
||||
|
||||
return Promise.resolve({
|
||||
rows: rows as O[],
|
||||
numAffectedRows: BigInt(changes),
|
||||
insertId: BigInt(lastInsertRowId),
|
||||
});
|
||||
}
|
||||
|
||||
// deno-lint-ignore require-yield
|
||||
async *streamQuery<R>(): AsyncIterableIterator<QueryResult<R>> {
|
||||
throw new Error('Sqlite driver doesn\'t support streaming');
|
||||
}
|
||||
}
|
||||
|
||||
class ConnectionMutex {
|
||||
#promise?: Promise<void>;
|
||||
#resolve?: () => void;
|
||||
|
||||
async lock(): Promise<void> {
|
||||
while (this.#promise) {
|
||||
await this.#promise;
|
||||
}
|
||||
|
||||
this.#promise = new Promise((resolve) => {
|
||||
this.#resolve = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
unlock(): void {
|
||||
const resolve = this.#resolve;
|
||||
|
||||
this.#promise = undefined;
|
||||
this.#resolve = undefined;
|
||||
|
||||
resolve?.();
|
||||
}
|
||||
}
|
||||
|
||||
export { DenoSqliteDriver };
|
64
db/type.ts
Normal file
64
db/type.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import { ColumnType, Generated, Insertable, Selectable, Updateable } from "kysely";
|
||||
|
||||
/**
|
||||
* "Code" TEXT,
|
||||
"Date" TEXT,
|
||||
"Close" INTEGER NOT NULL,
|
||||
"Diff" INTEGER NOT NULL,
|
||||
"Open" INTEGER NOT NULL,
|
||||
"High" INTEGER NOT NULL,
|
||||
"Low" INTEGER NOT NULL,
|
||||
"Volume" INTEGER NOT NULL,
|
||||
*/
|
||||
|
||||
export interface StockTable {
|
||||
Code: string;
|
||||
Date: string;
|
||||
Close: number;
|
||||
Diff: number;
|
||||
Open: number;
|
||||
High: number;
|
||||
Low: number;
|
||||
Volume: number;
|
||||
}
|
||||
|
||||
export interface KRXCorpTable{
|
||||
Name: string;
|
||||
/**
|
||||
* PK
|
||||
*/
|
||||
Code: string;
|
||||
|
||||
Sector: string;
|
||||
Product: string;
|
||||
ListingDay: string;
|
||||
ClosingMonth: string;
|
||||
Representative: string;
|
||||
Homepage: string;
|
||||
AddressArea: string;
|
||||
LastUpdate: string;
|
||||
}
|
||||
|
||||
export interface KOSPITable{
|
||||
Name: string;
|
||||
/**
|
||||
* PK
|
||||
*/
|
||||
Code: string;
|
||||
}
|
||||
|
||||
export interface KOSDAQTable{
|
||||
Name: string;
|
||||
/**
|
||||
* PK
|
||||
*/
|
||||
Code: string;
|
||||
}
|
||||
|
||||
|
||||
export interface Database {
|
||||
stock: StockTable;
|
||||
KRXCorp: KRXCorpTable;
|
||||
KOSPI: KOSPITable;
|
||||
KOSDAQ: KOSDAQTable;
|
||||
}
|
37
deno.json
37
deno.json
@ -2,31 +2,28 @@
|
||||
"lock": false,
|
||||
"tasks": {
|
||||
"start": "deno run -A --watch=static/,routes/ dev.ts",
|
||||
"update": "deno run -A -r https://fresh.deno.dev/update ."
|
||||
},
|
||||
"lint": {
|
||||
"rules": {
|
||||
"tags": [
|
||||
"fresh",
|
||||
"recommended"
|
||||
]
|
||||
}
|
||||
"prod_start": "deno run -A main.ts",
|
||||
"update": "deno run -A -r https://fresh.deno.dev/update .",
|
||||
"build": "deno run -A dev.ts build",
|
||||
"preview": "deno run -A main.ts"
|
||||
},
|
||||
"lint": { "rules": { "tags": ["fresh", "recommended"] } },
|
||||
"imports": {
|
||||
"$fresh/": "https://deno.land/x/fresh@1.3.1/",
|
||||
"preact": "https://esm.sh/preact@10.15.1",
|
||||
"preact/": "https://esm.sh/preact@10.15.1/",
|
||||
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.0",
|
||||
"@preact/signals": "https://esm.sh/*@preact/signals@1.1.3",
|
||||
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.2.3",
|
||||
"$fresh/": "https://deno.land/x/fresh@1.5.2/",
|
||||
"preact": "https://esm.sh/preact@10.18.1",
|
||||
"preact/": "https://esm.sh/preact@10.18.1/",
|
||||
"preact-render-to-string": "https://esm.sh/*preact-render-to-string@6.2.2",
|
||||
"@preact/signals": "https://esm.sh/*@preact/signals@1.2.1",
|
||||
"@preact/signals-core": "https://esm.sh/*@preact/signals-core@1.5.0",
|
||||
"auto-animate": "https://esm.sh/@formkit/auto-animate@0.7.0",
|
||||
"auto-animate/": "https://esm.sh/@formkit/auto-animate@0.7.0/",
|
||||
"twind": "https://esm.sh/twind@0.16.19",
|
||||
"twind/": "https://esm.sh/twind@0.16.19/",
|
||||
"$std/": "https://deno.land/std@0.193.0/"
|
||||
"$std/": "https://deno.land/std@0.203.0/",
|
||||
"kysely": "npm:kysely@^0.26.3",
|
||||
"kysely/helpers/sqlite": "npm:kysely@^0.26.3/helpers/sqlite",
|
||||
"sqlite": "https://deno.land/x/sqlite@v3.8/mod.ts"
|
||||
},
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact"
|
||||
}
|
||||
"compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "preact" },
|
||||
"exclude": ["**/_fresh/*"]
|
||||
}
|
||||
|
44
fresh.gen.ts
44
fresh.gen.ts
@ -1,36 +1,42 @@
|
||||
// DO NOT EDIT. This file is generated by fresh.
|
||||
// DO NOT EDIT. This file is generated by Fresh.
|
||||
// This file SHOULD be checked into source version control.
|
||||
// This file is automatically updated during development when running `dev.ts`.
|
||||
|
||||
import * as $0 from "./routes/_404.tsx";
|
||||
import * as $1 from "./routes/_app.tsx";
|
||||
import * as $2 from "./routes/api/joke.ts";
|
||||
import * as $3 from "./routes/api/kosdaq.ts";
|
||||
import * as $4 from "./routes/api/kospi.ts";
|
||||
import * as $5 from "./routes/api/pages/[name].ts";
|
||||
import * as $6 from "./routes/api/pages/index.ts";
|
||||
import * as $7 from "./routes/greet/[name].tsx";
|
||||
import * as $8 from "./routes/index.tsx";
|
||||
import * as $9 from "./routes/pages/[name].tsx";
|
||||
import * as $2 from "./routes/api/corps/[index].ts";
|
||||
import * as $3 from "./routes/api/corps/index.ts";
|
||||
import * as $4 from "./routes/api/joke.ts";
|
||||
import * as $5 from "./routes/api/kosdaq.ts";
|
||||
import * as $6 from "./routes/api/kospi.ts";
|
||||
import * as $7 from "./routes/api/pages/[name].ts";
|
||||
import * as $8 from "./routes/api/pages/index.ts";
|
||||
import * as $9 from "./routes/greet/[name].tsx";
|
||||
import * as $10 from "./routes/index.tsx";
|
||||
import * as $11 from "./routes/pages/[name].tsx";
|
||||
import * as $$0 from "./islands/Counter.tsx";
|
||||
import * as $$1 from "./islands/StockList.tsx";
|
||||
import * as $$1 from "./islands/Search.tsx";
|
||||
import * as $$2 from "./islands/StockList.tsx";
|
||||
|
||||
const manifest = {
|
||||
routes: {
|
||||
"./routes/_404.tsx": $0,
|
||||
"./routes/_app.tsx": $1,
|
||||
"./routes/api/joke.ts": $2,
|
||||
"./routes/api/kosdaq.ts": $3,
|
||||
"./routes/api/kospi.ts": $4,
|
||||
"./routes/api/pages/[name].ts": $5,
|
||||
"./routes/api/pages/index.ts": $6,
|
||||
"./routes/greet/[name].tsx": $7,
|
||||
"./routes/index.tsx": $8,
|
||||
"./routes/pages/[name].tsx": $9,
|
||||
"./routes/api/corps/[index].ts": $2,
|
||||
"./routes/api/corps/index.ts": $3,
|
||||
"./routes/api/joke.ts": $4,
|
||||
"./routes/api/kosdaq.ts": $5,
|
||||
"./routes/api/kospi.ts": $6,
|
||||
"./routes/api/pages/[name].ts": $7,
|
||||
"./routes/api/pages/index.ts": $8,
|
||||
"./routes/greet/[name].tsx": $9,
|
||||
"./routes/index.tsx": $10,
|
||||
"./routes/pages/[name].tsx": $11,
|
||||
},
|
||||
islands: {
|
||||
"./islands/Counter.tsx": $$0,
|
||||
"./islands/StockList.tsx": $$1,
|
||||
"./islands/Search.tsx": $$1,
|
||||
"./islands/StockList.tsx": $$2,
|
||||
},
|
||||
baseUrl: import.meta.url,
|
||||
};
|
||||
|
53
gen.py
53
gen.py
@ -5,7 +5,6 @@ import sqlite3
|
||||
from typing import Dict, List
|
||||
from render import *
|
||||
import db as database
|
||||
from jinja2 import Environment, PackageLoader, select_autoescape
|
||||
import pandas as pd
|
||||
import tqdm
|
||||
|
||||
@ -92,7 +91,7 @@ def isMACDCrossSignal(signal: pd.Series, macd: pd.Series, nday: int, order=1) ->
|
||||
signal.iloc[nday+order] < macd.iloc[nday+order])
|
||||
|
||||
def isRelativeDiffLessThan(a:pd.Series,b:pd.Series, threshold: float,nday:int) -> bool:
|
||||
return abs(a.iloc[nday] - b.iloc[nday]) / b.iloc[nday] < threshold
|
||||
return abs(a.iloc[nday] - b.iloc[nday + 1]) / b.iloc[nday + 1] < threshold
|
||||
|
||||
def isDiffGreaterThan(a:pd.Series,b:pd.Series, nday:int) -> bool:
|
||||
"""a is bigger than b"""
|
||||
@ -145,7 +144,6 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
|
||||
d5 = d(5)
|
||||
d20 = d(20)
|
||||
d25 = d(25)
|
||||
d30 = d(30)
|
||||
d45 = d(45)
|
||||
d60 = d(60)
|
||||
d120 = d(120)
|
||||
@ -156,8 +154,14 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
|
||||
|
||||
bollinger_upperband = d25 + 2* d_std25
|
||||
|
||||
a = [d5, d20, d45, d60]
|
||||
a = [d5, d20, d45]
|
||||
for nday in ndays:
|
||||
if openv[nday] <= d240[nday] and d240[nday] <= close[nday] and d240[nday + 1] < d240[nday]:
|
||||
collector.collect("양봉사이240일선증가", corp, stock.index[nday])
|
||||
|
||||
if d5[nday + 1] < d5[nday] and d5[nday + 2] > d5[nday + 1] and d20[nday + 1] < d20[nday]:
|
||||
collector.collect("5일선반등120선증가", corp, stock.index[nday])
|
||||
|
||||
if openv[nday] <= d20[nday] and d20[nday] <= close[nday]:
|
||||
collector.collect("양봉사이20일선", corp, stock.index[nday])
|
||||
|
||||
@ -207,6 +211,9 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
|
||||
if (D240BiggerThanYesterDay):
|
||||
collector.collect("240일 증가", corp, stock.index[nday])
|
||||
|
||||
if max([d[nday] for d in (d20, d60, d120)]) < min(close[nday], openv[nday]):
|
||||
collector.collect("떠있음", corp, stock.index[nday])
|
||||
|
||||
if (d60[nday + 1] < d60[nday]):
|
||||
collector.collect("정배열60", corp, stock.index[nday])
|
||||
if (d20[nday + 1] < d20[nday]):
|
||||
@ -217,6 +224,11 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
|
||||
d120[nday + 1] <= d120[nday]):
|
||||
collector.collect("모두 정배열", corp, stock.index[nday])
|
||||
|
||||
if(d120[nday + 1] <= d120[nday] and
|
||||
d120[nday + 1] < d240[nday] and
|
||||
d120[nday] >= d240[nday]):
|
||||
collector.collect("120선240선추월", corp, stock.index[nday])
|
||||
|
||||
if (d5[nday + 1] < d20[nday + 1] and d20[nday] < d5[nday]):
|
||||
collector.collect("d20d5돌파", corp, stock.index[nday])
|
||||
|
||||
@ -248,8 +260,6 @@ def collect(data: DataStore, collector: OutputCollector, corp: database.KRXCorp
|
||||
#rsi_signal = macd.loc[::-1].ewm(span=7).mean().loc[::-1]
|
||||
|
||||
parser = argparse.ArgumentParser(description="주식 검색 정보를 출력합니다.")
|
||||
parser.add_argument("--format", "-f", choices=["json", "html"], default="html",
|
||||
help="출력 포맷을 지정합니다. 기본값은 html입니다.")
|
||||
parser.add_argument("--dir", "-d", default=".", help="출력할 폴더를 지정합니다.")
|
||||
parser.add_argument("--corp", "-c", help="주식 코드를 지정합니다. 지정하지 않으면 kosdaq과 kospi만 검색합니다.")
|
||||
parser.add_argument("--fullSearch", help="모든 주식을 검색합니다.", action='store_true')
|
||||
@ -268,10 +278,6 @@ if __name__ == "__main__":
|
||||
if args.corp:
|
||||
krx_corps = [corp for corp in krx_corps if corp.Code == args.corp]
|
||||
|
||||
env = Environment(
|
||||
loader=PackageLoader('render', 'templates'),
|
||||
autoescape=select_autoescape(['html', 'xml'])
|
||||
)
|
||||
collector = OutputCollector()
|
||||
prepareCollector(collector)
|
||||
|
||||
@ -281,25 +287,10 @@ if __name__ == "__main__":
|
||||
dataStore.clearCache()
|
||||
|
||||
for k,v in collector.data.items():
|
||||
if args.format == "json":
|
||||
data = json.dumps(v.toDict(), indent=4, ensure_ascii=False)
|
||||
if args.printStdout:
|
||||
print(k)
|
||||
print(data)
|
||||
else:
|
||||
with open(os.path.join(args.dir, k + ".json"), "w", encoding="UTF-8") as f:
|
||||
f.write(data)
|
||||
data = json.dumps(v.toDict(), ensure_ascii=False)
|
||||
if args.printStdout:
|
||||
print(k)
|
||||
print(data)
|
||||
else:
|
||||
template = env.get_template("Lists.html")
|
||||
|
||||
days = v.corpListByDate.keys()
|
||||
days = list(days)
|
||||
days.sort(reverse=True)
|
||||
days = days[:5]
|
||||
|
||||
html = template.render(collected=v, title=k, days=days, lastUpdate=datetime.date.today().isoformat())
|
||||
if args.printStdout:
|
||||
print(html)
|
||||
else:
|
||||
with open(os.path.join(args.dir, k + ".html"), "w", encoding="UTF-8") as f:
|
||||
f.write(html)
|
||||
with open(os.path.join(args.dir, k + ".json"), "w", encoding="UTF-8") as f:
|
||||
f.write(data)
|
5
islands/Search.tsx
Normal file
5
islands/Search.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
export default function Search(){
|
||||
return <div>
|
||||
<div>div</div>
|
||||
</div>
|
||||
}
|
@ -1,9 +1,18 @@
|
||||
import { Button } from "../components/Button.tsx";
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
import { useEffect, useLayoutEffect, useRef } from "preact/hooks";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { Signal, useSignal } from "@preact/signals";
|
||||
import { IS_BROWSER } from "$fresh/runtime.ts";
|
||||
import { mapValues } from "$std/collections/map_values.ts";
|
||||
import { useAsync } from "../util/util.ts";
|
||||
import {
|
||||
Coperation,
|
||||
CorpSimple,
|
||||
fetchKosdaqList,
|
||||
fetchKospiList,
|
||||
fetchPageInfo,
|
||||
PageCorpsInfo,
|
||||
} from "../util/api.ts";
|
||||
|
||||
interface StockProps {
|
||||
pageName: string;
|
||||
@ -31,94 +40,70 @@ function ToggleButton(props: ToggleButtonProps) {
|
||||
);
|
||||
}
|
||||
|
||||
type QueryStatus<T> = {
|
||||
type: "loading";
|
||||
} | {
|
||||
type: "complete";
|
||||
data: T;
|
||||
} | {
|
||||
type: "error";
|
||||
err: Error;
|
||||
};
|
||||
|
||||
function useAsync<T>(fn: () => Promise<T>): Signal<QueryStatus<T>> {
|
||||
const state = useSignal({
|
||||
type: "loading",
|
||||
} as QueryStatus<T>);
|
||||
function StockListByDate(
|
||||
{ prevSet, rows, name }: {
|
||||
prevSet: Set<string>;
|
||||
rows: Coperation[];
|
||||
name: string;
|
||||
},
|
||||
) {
|
||||
const lastCount = useRef(rows.length);
|
||||
const curCount = rows.length;
|
||||
const parent = useRef<HTMLDivElement>(null);
|
||||
const controller = useRef<
|
||||
{
|
||||
isEnabled: () => boolean;
|
||||
disable: () => void;
|
||||
enable: () => void;
|
||||
} | undefined
|
||||
>();
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const data = await fn();
|
||||
state.value = {
|
||||
type: "complete",
|
||||
data: data,
|
||||
};
|
||||
} catch (err) {
|
||||
state.value = {
|
||||
type: "error",
|
||||
err: err,
|
||||
};
|
||||
console.log("animation mount on ", name);
|
||||
const { default: autoAnimate } = await import(
|
||||
"https://esm.sh/@formkit/auto-animate@0.7.0"
|
||||
);
|
||||
if (parent.current) {
|
||||
const cntr = autoAnimate(parent.current);
|
||||
controller.current = cntr;
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
return state;
|
||||
}
|
||||
}, [parent]);
|
||||
|
||||
interface Coperation {
|
||||
Name: string;
|
||||
Code: string;
|
||||
Sector: string;
|
||||
Product: string;
|
||||
ListingDay: string;
|
||||
ClosingMonth: string;
|
||||
Representative: string;
|
||||
Homepage: string;
|
||||
AddressArea: string;
|
||||
LastUpdate: string;
|
||||
}
|
||||
useLayoutEffect(() => {
|
||||
if (controller.current) {
|
||||
if (Math.abs(curCount - lastCount.current) > 200) {
|
||||
console.log("disable animation", curCount, "from", lastCount.current);
|
||||
controller.current.disable();
|
||||
} else {
|
||||
console.log("enable animation", curCount, "from", lastCount.current);
|
||||
controller.current.enable();
|
||||
}
|
||||
lastCount.current = curCount;
|
||||
}
|
||||
}, [parent, rows]);
|
||||
|
||||
interface PageCorpsInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
corpListByDate: Record<string, Coperation[]>;
|
||||
}
|
||||
|
||||
interface CorpSimple {
|
||||
code: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
function StockListByDate({prevSet, rows, name}:{prevSet:Set<string>,
|
||||
rows:Coperation[],
|
||||
name: string}){
|
||||
const parent = useRef<HTMLDivElement>(null);
|
||||
useEffect(()=>{
|
||||
(async ()=>{
|
||||
console.log("animation mount on ",name);
|
||||
const {default:autoAnimate} = await import("https://esm.sh/@formkit/auto-animate@0.7.0");
|
||||
parent.current && autoAnimate(parent.current)
|
||||
})();
|
||||
},[parent]);
|
||||
|
||||
return <div ref={parent}>
|
||||
<h2 class="text-lg">{name}</h2>
|
||||
{rows.map((row) => {
|
||||
const firstOccur = !prevSet.has(row.Code);
|
||||
return (
|
||||
<div
|
||||
key={row.Code}
|
||||
class={[
|
||||
"bg-white",
|
||||
firstOccur ? "text-[#ff5454]" : "text-black",
|
||||
].join(" ")}
|
||||
>
|
||||
<a href={`https://stockplus.com/m/stocks/KOREA-A${row.Code}`}>
|
||||
{row.Name}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
return (
|
||||
<div ref={parent}>
|
||||
<h2 class="text-lg">{name}</h2>
|
||||
{rows.map((row) => {
|
||||
const firstOccur = !prevSet.has(row.Code);
|
||||
return (
|
||||
<div
|
||||
key={row.Code}
|
||||
class={[
|
||||
"bg-white",
|
||||
firstOccur ? "text-[#ff5454] underline" : "text-black",
|
||||
].join(" ")}
|
||||
>
|
||||
<a href={`https://stockplus.com/m/stocks/KOREA-A${row.Code}`}>
|
||||
{row.Name}
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StockList({ data }: { data: PageCorpsInfo }) {
|
||||
@ -135,14 +120,13 @@ function StockList({ data }: { data: PageCorpsInfo }) {
|
||||
const prevSet = i == 0 ? new Set<string>() : sets[i - 1];
|
||||
const rows = corpListByDate[x];
|
||||
return (
|
||||
<StockListByDate key={x} name={x} prevSet={prevSet} rows={rows}></StockListByDate>
|
||||
<StockListByDate key={x} name={x} prevSet={prevSet} rows={rows} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
type FilterInfoOption = {
|
||||
list: {
|
||||
items: CorpSimple[];
|
||||
@ -155,7 +139,7 @@ function filterInfo(info: Coperation[], filterList: FilterInfoOption) {
|
||||
const checkMap = new Map<string, boolean>();
|
||||
for (const l of filterList.list) {
|
||||
for (const i of l.items) {
|
||||
checkMap.set(i.code, l.include);
|
||||
checkMap.set(i.Code, l.include);
|
||||
}
|
||||
}
|
||||
return info.filter((x) => {
|
||||
@ -169,17 +153,13 @@ function filterInfo(info: Coperation[], filterList: FilterInfoOption) {
|
||||
}
|
||||
|
||||
export default function StockListUI(props: StockProps) {
|
||||
const sig = useAsync<[PageCorpsInfo, CorpSimple[], CorpSimple[]]>(async () => {
|
||||
const res = await Promise.all([
|
||||
fetch("/api/pages/" + encodeURIComponent(props.pageName)),
|
||||
fetch("/api/kospi"),
|
||||
fetch("/api/kosdaq"),
|
||||
]);
|
||||
const corpsInfo = await res[0].json() as PageCorpsInfo;
|
||||
const kospi = await res[1].json();
|
||||
const kosdaq = await res[2].json();
|
||||
return [corpsInfo, kospi, kosdaq];
|
||||
});
|
||||
const sig = useAsync<[PageCorpsInfo, CorpSimple[], CorpSimple[]]>(() =>
|
||||
Promise.all([
|
||||
fetchPageInfo(props.pageName),
|
||||
fetchKospiList(),
|
||||
fetchKosdaqList(),
|
||||
])
|
||||
);
|
||||
const viewKospi = useSignal(true);
|
||||
const viewKosdaq = useSignal(false);
|
||||
const viewOtherwise = useSignal(false);
|
||||
@ -203,34 +183,47 @@ export default function StockListUI(props: StockProps) {
|
||||
<p>File Loading Failed</p>
|
||||
</div>
|
||||
)
|
||||
: <StockList data={applyFilter(sig.value.data[0], sig.value.data[1], sig.value.data[2])}></StockList>}
|
||||
: (
|
||||
<StockList
|
||||
data={applyFilter(
|
||||
sig.value.data[0],
|
||||
sig.value.data[1],
|
||||
sig.value.data[2],
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
function applyFilter(data: PageCorpsInfo, kospi: CorpSimple[], kosdaq: CorpSimple[]): PageCorpsInfo{
|
||||
const filter = getFilters(kospi,kosdaq);
|
||||
function applyFilter(
|
||||
data: PageCorpsInfo,
|
||||
kospi: CorpSimple[],
|
||||
kosdaq: CorpSimple[],
|
||||
): PageCorpsInfo {
|
||||
const filter = getFilters(kospi, kosdaq);
|
||||
return {
|
||||
name: data.name,
|
||||
description: data.description,
|
||||
corpListByDate: mapValues(data.corpListByDate, (it: Coperation[])=>{
|
||||
corpListByDate: mapValues(data.corpListByDate, (it: Coperation[]) => {
|
||||
return filterInfo(it, filter);
|
||||
})
|
||||
}
|
||||
}),
|
||||
};
|
||||
}
|
||||
function getFilters(kospi: CorpSimple[], kosdaq: CorpSimple[]): FilterInfoOption{
|
||||
function getFilters(
|
||||
kospi: CorpSimple[],
|
||||
kosdaq: CorpSimple[],
|
||||
): FilterInfoOption {
|
||||
return {
|
||||
otherwise: viewOtherwise.value,
|
||||
list: [{
|
||||
include: viewKospi.value,
|
||||
items: kospi
|
||||
},
|
||||
{
|
||||
include: viewKosdaq.value,
|
||||
items: kosdaq
|
||||
}
|
||||
]
|
||||
}
|
||||
include: viewKospi.value,
|
||||
items: kospi,
|
||||
}, {
|
||||
include: viewKosdaq.value,
|
||||
items: kosdaq,
|
||||
}],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
8
main.ts
8
main.ts
@ -9,9 +9,15 @@ import "$std/dotenv/load.ts";
|
||||
import { start } from "$fresh/server.ts";
|
||||
import manifest from "./fresh.gen.ts";
|
||||
|
||||
import twindPlugin from "$fresh/plugins/twind.ts";
|
||||
import twindPlugin from "$fresh/plugins/twindv1.ts";
|
||||
import twindConfig from "./twind.config.ts";
|
||||
|
||||
console.log("start");
|
||||
|
||||
Deno.addSignalListener("SIGINT", () => {
|
||||
Deno.exit(0);
|
||||
});
|
||||
|
||||
await start(manifest, {
|
||||
port: 12001,
|
||||
plugins: [twindPlugin(twindConfig)]
|
||||
|
15
pages.ts
15
pages.ts
@ -26,18 +26,25 @@ function watchFile(
|
||||
}
|
||||
}
|
||||
})();
|
||||
Deno.addSignalListener("SIGINT", () => {
|
||||
const closeHandler = () => {
|
||||
watcherRef.close();
|
||||
});
|
||||
};
|
||||
Deno.addSignalListener("SIGINT", closeHandler);
|
||||
return ()=>{
|
||||
Deno.removeSignalListener("SIGINT", closeHandler);
|
||||
closeHandler();
|
||||
}
|
||||
}
|
||||
|
||||
let pages_meta: PageDescription[] = [];
|
||||
let mtime = 0;
|
||||
let lastest_disposer = () => {};
|
||||
export async function get_pages_meta(): Promise<[PageDescription[],number]>{
|
||||
if (pages_meta) {
|
||||
if (pages_meta.length == 0) {
|
||||
pages_meta = await readPagesDescription();
|
||||
mtime = Date.now();
|
||||
watchFile(PAGES_PATH, async () => {
|
||||
lastest_disposer();
|
||||
lastest_disposer = watchFile(PAGES_PATH, async () => {
|
||||
pages_meta = await readPagesDescription();
|
||||
mtime = Date.now();
|
||||
});
|
||||
|
16
pages.yaml
16
pages.yaml
@ -36,11 +36,11 @@
|
||||
macd가 아래로 내려가는 시점을 찾습니다. macd 는 5일선과 10일선으로 이루어지고
|
||||
시그널을 구하기 위한 이동 평균은 4일입니다.
|
||||
- name: 뭉침
|
||||
description: d5, d20, d45, d60 만난것 종가 5% 이내
|
||||
description: d5, d20, d45 만난것 종가 5% 이내
|
||||
- name: 뭉침01
|
||||
description: d5, d20, d45, d60 만난것 종가 1% 이내
|
||||
description: d5, d20, d45 만난것 종가 1% 이내
|
||||
- name: 뭉침03
|
||||
description: d5, d20, d45, d60 만난것 종가 3% 이내
|
||||
description: d5, d20, d45 만난것 종가 3% 이내
|
||||
|
||||
- name: 45일선 반등
|
||||
description: 45일 선반등
|
||||
@ -64,3 +64,13 @@
|
||||
description: '볼린저 밴드(25일선 ,표준편차 2배)의 위 밴드 값을 넘었을 때 표시.'
|
||||
- name: 양봉사이20일선
|
||||
description: Open과 Close 사이 20일 선
|
||||
- name: 양봉사이240일선증가
|
||||
description: Open과 Close 사이 240일 선. 240일 선 증가
|
||||
- name: 떠있음
|
||||
description: |
|
||||
양봉, 음봉이 20일선, 60일선, 120선보다 떠있으면
|
||||
- name: 5일선반등120선증가
|
||||
description: 5일선이 반등 120 선 증가
|
||||
- name: 120선240선추월
|
||||
description: |
|
||||
120선이 상승해서 240일 선을 뚫을때
|
35
routes/api/corps/[index].ts
Normal file
35
routes/api/corps/[index].ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { db } from "../../../db/db.ts";
|
||||
import { jsonArrayFrom } from "kysely/helpers/sqlite";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET(req, ctx): Promise<Response> {
|
||||
const headers = new Headers({
|
||||
"content-type": "application/json"
|
||||
});
|
||||
const index = ctx.params.index;
|
||||
const corp = await db.selectFrom("KRXCorp")
|
||||
.selectAll([
|
||||
"KRXCorp"
|
||||
])
|
||||
.select(eb=> [
|
||||
jsonArrayFrom(eb.selectFrom("stock")
|
||||
.select([
|
||||
"stock.Close",
|
||||
"stock.Open",
|
||||
"stock.Low",
|
||||
"stock.High",
|
||||
"stock.Date",
|
||||
"stock.Volume",
|
||||
])
|
||||
.where("Code", "=", index)
|
||||
.orderBy("Date", "desc")
|
||||
.limit(100)
|
||||
).as("prices")]
|
||||
)
|
||||
.where("Code", "=", index)
|
||||
.executeTakeFirst();
|
||||
|
||||
return new Response(JSON.stringify(corp ?? null), {headers});
|
||||
},
|
||||
}
|
21
routes/api/corps/index.ts
Normal file
21
routes/api/corps/index.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import { db } from "../../../db/db.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET(req, _ctx): Promise<Response> {
|
||||
const headers = new Headers({
|
||||
"content-type": "application/json"
|
||||
});
|
||||
const url = new URL(req.url);
|
||||
const q = url.searchParams.get("q");
|
||||
const name = url.searchParams.get("name");
|
||||
const corps = await db.selectFrom("KRXCorp")
|
||||
.selectAll([
|
||||
"KRXCorp"
|
||||
])
|
||||
.$if(!!q, qb=> qb.where("Name", "like", "%"+q+"%"))
|
||||
.$if(!!name, qb => qb.where("Name", "=", name))
|
||||
.execute();
|
||||
return new Response(JSON.stringify(corps), {headers});
|
||||
},
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import {DB} from "https://deno.land/x/sqlite/mod.ts";
|
||||
import { db } from "../../db/db.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET(req, _ctx): Promise<Response> {
|
||||
const headers = new Headers({
|
||||
"content-type": "application/json"
|
||||
});
|
||||
const db = new DB("stock.db");
|
||||
const conn = db.query("SELECT Code,Name FROM KOSDAQ");
|
||||
const body = conn.map(row=>({
|
||||
code: row[0],
|
||||
name: row[1]
|
||||
}))
|
||||
return new Response(JSON.stringify(body), {headers});
|
||||
const rows = await db.selectFrom("KOSDAQ")
|
||||
.select([
|
||||
"Code",
|
||||
"Name"
|
||||
])
|
||||
.execute();
|
||||
return new Response(JSON.stringify(rows), {headers});
|
||||
},
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
import { Handlers } from "$fresh/server.ts";
|
||||
import {DB} from "https://deno.land/x/sqlite/mod.ts";
|
||||
import { db } from "../../db/db.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET(req, _ctx): Promise<Response> {
|
||||
const headers = new Headers({
|
||||
"content-type": "application/json"
|
||||
});
|
||||
const db = new DB("stock.db");
|
||||
const conn = db.query("SELECT Code,Name FROM KOSPI");
|
||||
const body = conn.map(row=>({
|
||||
code: row[0],
|
||||
name: row[1]
|
||||
}))
|
||||
return new Response(JSON.stringify(body), {headers});
|
||||
const rows = await db.selectFrom("KOSPI")
|
||||
.select([
|
||||
"Code",
|
||||
"Name"
|
||||
])
|
||||
.execute();
|
||||
return new Response(JSON.stringify(rows), {headers});
|
||||
},
|
||||
}
|
@ -25,16 +25,18 @@ export const handler: Handlers = {
|
||||
const mtime = stat.mtime ?? new Date(0);
|
||||
const body = await Deno.readTextFile(path);
|
||||
headers.set("last-modified", mtime.toUTCString());
|
||||
console.log(mtime);
|
||||
// headers.set("cache-control", "max-age=600");
|
||||
|
||||
const ifModifiedSinceValue = req.headers.get("if-modified-since");
|
||||
if ( ifModifiedSinceValue &&
|
||||
mtime.getTime() != new Date(ifModifiedSinceValue).getTime()
|
||||
){
|
||||
return new Response(null, {
|
||||
status: Status.NotModified,
|
||||
statusText: STATUS_TEXT[Status.NotModified]
|
||||
})
|
||||
}
|
||||
// const ifModifiedSinceValue = req.headers.get("if-modified-since");
|
||||
// if ( ifModifiedSinceValue &&
|
||||
// mtime.getTime() <= new Date(ifModifiedSinceValue).getTime()
|
||||
// ){
|
||||
// return new Response(null, {
|
||||
// status: Status.NotModified,
|
||||
// statusText: STATUS_TEXT[Status.NotModified]
|
||||
// })
|
||||
// }
|
||||
return new Response(body, {headers});
|
||||
},
|
||||
};
|
||||
|
@ -1,25 +1,21 @@
|
||||
import { Head } from "$fresh/runtime.ts";
|
||||
import { useSignal } from "@preact/signals";
|
||||
import {Button} from "../components/Button.tsx";
|
||||
import { PageDescription, get_pages_meta } from "../pages.ts";
|
||||
import { get_pages_meta, PageDescription } from "../pages.ts";
|
||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET(_req, ctx){
|
||||
const [pages,_] = await get_pages_meta();
|
||||
async GET(_req, ctx) {
|
||||
const [pages, _] = await get_pages_meta();
|
||||
return await ctx.render(pages);
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export default function Home({data}: PageProps<PageDescription[]>) {
|
||||
const count = useSignal(3);
|
||||
|
||||
export default function Home({ data }: PageProps<PageDescription[]>) {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>stock-front</title>
|
||||
</Head>
|
||||
<div class="px-4 py-8 mx-auto bg-[#86efac]">
|
||||
<div class="px-4 py-8 mx-auto bg-[#86efac] min-h-screen">
|
||||
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
|
||||
<img
|
||||
class="my-6"
|
||||
@ -31,14 +27,16 @@ export default function Home({data}: PageProps<PageDescription[]>) {
|
||||
<h1 class="text-4xl font-bold">Stock</h1>
|
||||
<div class="my-4">
|
||||
<ul>
|
||||
{
|
||||
data.map(x=><li class="my-2">
|
||||
<a class="p-2 block hover:bg-gray-300 bg-white rounded" href={`/pages/${encodeURIComponent(x.name)}`}>
|
||||
{data.map((x) => (
|
||||
<li class="my-2">
|
||||
<a
|
||||
class="p-2 block hover:bg-gray-300 bg-white rounded"
|
||||
href={`/pages/${encodeURIComponent(x.name)}`}
|
||||
>
|
||||
{x.name}
|
||||
</a>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,14 +1,26 @@
|
||||
import { PageProps } from "$fresh/server.ts";
|
||||
import { PageProps, Handlers } from "$fresh/server.ts";
|
||||
import { Head } from "$fresh/runtime.ts";
|
||||
import { get_pages_meta, PageDescription } from "../../pages.ts";
|
||||
import StockList from "../../islands/StockList.tsx";
|
||||
|
||||
export const handler: Handlers = {
|
||||
async GET(_req, ctx) {
|
||||
const [pages, _] = await get_pages_meta();
|
||||
const name = ctx.params.name;
|
||||
const page = pages.filter(x=> x.name === name);
|
||||
if (page.length === 0) {
|
||||
return await ctx.renderNotFound();
|
||||
}
|
||||
return await ctx.render(page[0]);
|
||||
},
|
||||
};
|
||||
|
||||
export default function Pages(props: PageProps) {
|
||||
export default function Pages(props: PageProps<PageDescription>) {
|
||||
return <>
|
||||
<Head>
|
||||
<title>Stock: {props.params.name}</title>
|
||||
</Head>
|
||||
<div class="px-4 py-8 mx-auto bg-[#86efac]">
|
||||
<div class="px-4 py-8 mx-auto bg-[#86efac] min-h-screen">
|
||||
<div class="max-w-screen-md mx-auto flex flex-col items-center justify-center">
|
||||
<img
|
||||
class="my-6"
|
||||
@ -18,6 +30,7 @@ export default function Pages(props: PageProps) {
|
||||
alt="stock graph"
|
||||
/>
|
||||
<h1 class="text-4xl">{props.params.name}</h1>
|
||||
<p>{props.data.description}</p>
|
||||
<StockList pageName={props.params.name}></StockList>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,60 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Stock</title>
|
||||
<style>
|
||||
body{
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(to right, #2b2b2b, #3d1844);
|
||||
color: #fff;
|
||||
}
|
||||
.table_item:nth-child(2n){
|
||||
background: #a7a7a7;
|
||||
}
|
||||
.table_item:nth-child(2n+1){
|
||||
background: #fff;
|
||||
}
|
||||
.table_item:hover{
|
||||
background: #8d8d8d;
|
||||
}
|
||||
.container{
|
||||
display: grid;
|
||||
grid-template-rows: 24px auto;
|
||||
background: #f0f0f0;
|
||||
color: black;
|
||||
box-shadow: 0px 0px 5px 0px white;
|
||||
text-decoration: none;
|
||||
grid-template-columns: repeat({{ 5 }}, 1fr);
|
||||
}
|
||||
.container a:link, a:visited{
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
}
|
||||
.data_header{
|
||||
border-bottom: 1px solid #a7a7a7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="margin: auto; max-width: 750px;">
|
||||
<h1>{{title}} Stock List</h1>
|
||||
<h3>{{lastUpdate}}</h3>
|
||||
<section class="description">
|
||||
{{collected.description}}
|
||||
</section>
|
||||
<section class="container">
|
||||
{% for day in days|reverse %}
|
||||
<div class="data_header">{{ day }}</div>
|
||||
{% endfor %}
|
||||
{% for day in days|reverse %}
|
||||
{% set corplist = collected.corpListByDate[day] %}
|
||||
<div>{% for item in corplist %}
|
||||
<div class="table_item"><a href="https://stockplus.com/m/stocks/KOREA-A{{ item.Code }}">{{ item.Name }}({{item.Code}})</a></div>{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</section>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,40 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Stock</title>
|
||||
<style>
|
||||
body{
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
background: linear-gradient(to right, #2b2b2b, #3d1844);
|
||||
color: #fff;
|
||||
}
|
||||
.container{
|
||||
display: grid;
|
||||
background: #f0f0f0;
|
||||
color: black;
|
||||
box-shadow: 0px 0px 5px 0px white;
|
||||
text-decoration: none;
|
||||
}
|
||||
.container a:link, a:visited{
|
||||
text-decoration: none;
|
||||
color: black;
|
||||
font-size: 40px;
|
||||
}
|
||||
.data_header{
|
||||
border-bottom: 1px solid #a7a7a7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div style="margin: auto; max-width: 750px;">
|
||||
<h1>Main</h1>
|
||||
<div class="container">
|
||||
{% for p in pages %}
|
||||
<a href="/dist/{{p.name}}.html">{{p.name}}</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
@ -1,5 +1,10 @@
|
||||
import { Options } from "$fresh/plugins/twind.ts";
|
||||
import { defineConfig, Preset } from "https://esm.sh/@twind/core@1.1.3";
|
||||
import presetTailwind from "https://esm.sh/@twind/preset-tailwind@1.1.4";
|
||||
import presetAutoprefix from "https://esm.sh/@twind/preset-autoprefix@1.0.7";
|
||||
|
||||
export default {
|
||||
...defineConfig({
|
||||
presets: [presetTailwind() as Preset, presetAutoprefix()],
|
||||
}),
|
||||
selfURL: import.meta.url,
|
||||
} as Options;
|
||||
}
|
||||
|
38
util/api.ts
Normal file
38
util/api.ts
Normal file
@ -0,0 +1,38 @@
|
||||
export interface Coperation {
|
||||
Name: string;
|
||||
Code: string;
|
||||
Sector: string;
|
||||
Product: string;
|
||||
ListingDay: string;
|
||||
ClosingMonth: string;
|
||||
Representative: string;
|
||||
Homepage: string;
|
||||
AddressArea: string;
|
||||
LastUpdate: string;
|
||||
}
|
||||
|
||||
export interface PageCorpsInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
corpListByDate: Record<string, Coperation[]>;
|
||||
}
|
||||
|
||||
export interface CorpSimple {
|
||||
Code: string;
|
||||
Name: string;
|
||||
}
|
||||
|
||||
export async function fetchPageInfo(pageName: string): Promise<PageCorpsInfo>{
|
||||
const res = await fetch("/api/pages/" + encodeURIComponent(pageName));
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export async function fetchKospiList(): Promise<CorpSimple[]>{
|
||||
const res = await fetch("/api/kospi");
|
||||
return await res.json();
|
||||
}
|
||||
|
||||
export async function fetchKosdaqList(): Promise<CorpSimple[]> {
|
||||
const res = await fetch("/api/kosdaq");
|
||||
return await res.json();
|
||||
}
|
35
util/util.ts
Normal file
35
util/util.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Signal, useSignal } from "@preact/signals";
|
||||
import { useEffect } from "preact/hooks";
|
||||
|
||||
export type QueryStatus<T> = {
|
||||
type: "loading";
|
||||
} | {
|
||||
type: "complete";
|
||||
data: T;
|
||||
} | {
|
||||
type: "error";
|
||||
err: Error;
|
||||
};
|
||||
|
||||
export function useAsync<T>(fn: () => Promise<T>): Signal<QueryStatus<T>> {
|
||||
const state = useSignal({
|
||||
type: "loading",
|
||||
} as QueryStatus<T>);
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
try {
|
||||
const data = await fn();
|
||||
state.value = {
|
||||
type: "complete",
|
||||
data: data,
|
||||
};
|
||||
} catch (err) {
|
||||
state.value = {
|
||||
type: "error",
|
||||
err: err,
|
||||
};
|
||||
}
|
||||
})();
|
||||
}, []);
|
||||
return state;
|
||||
}
|
Loading…
Reference in New Issue
Block a user