ผู้สร้างแรงบันดาลใจที่อยู่เบื้องหลังการพัฒนาบริการสถิติสำหรับ Tinkoff Investments คือ:
- บทความเกี่ยวกับHabré “สิ่งที่ Tinkoff Investments ไม่ได้พูด”
- การวิเคราะห์ความปรารถนา ของ ผู้ใช้แพลตฟอร์ม
- บทความเกี่ยวกับการคำนวณค่าคอมมิชชั่น
- จะคุยอะไรกัน?
- การพัฒนาบริการสถิติทีละขั้นตอน:
- การเชื่อมต่อกับ Tinkoff Invest API
- ดึงข้อมูลจาก Tinkoff Invest API ในเบราว์เซอร์
- รับรายงานนายหน้าและธุรกรรม
- GetBrokerReport
- วิธีการรับวันที่โดยคำนึงถึงการลบจากวันที่ปัจจุบัน
- คำขอสร้างรายงาน
- ผลลัพธ์:
- รับเงินปันผลผู้ออกต่างประเทศ
- GetOperationsByCursor
- การคำนวณและการส่งออกข้อมูลที่น่าสนใจ
- ทำงานกับราคา
- ต้นทุนของสัญญาซื้อขายล่วงหน้า
- ตลาด อ.ต.ก
- การดำเนินการทางคณิตศาสตร์เกี่ยวกับเครื่องมือ
- ไมโครเซอร์วิสพร้อมแล้ว!
- ข้อสรุปและแผนสำหรับอนาคต
- https://opexbot.info
จะคุยอะไรกัน?
- เฉพาะส่วนที่ประยุกต์เกี่ยวกับการพัฒนา
- ความรู้และประสบการณ์จริงซึ่งมีความสำคัญมากในการทำงานกับเครื่องมือทางการเงิน
- ภาพรวมของปัญหาในการทำงาน
ดังนั้นฉันต้องการคำนวณสถิติการค้าและทำในวิธีที่สะดวก
การพัฒนาบริการสถิติทีละขั้นตอน:
- การเชื่อมต่อกับ Tinkoff Invest API
- ดึงข้อมูลจาก Tinkoff Invest API ในเบราว์เซอร์
- รับรายงานนายหน้าและธุรกรรม
- การคำนวณและการส่งออกข้อมูลที่น่าสนใจ
- ข้อสรุปและแผนสำหรับอนาคต
การเชื่อมต่อกับ Tinkoff Invest API
หากต้องการ เชื่อมต่อกับ API คุณสามารถใช้ sdk ใดก็ได้จากเอกสารhttps://github.com/Tinkoff/investAPI#sdk หรือแพ็คเกจ npm ` tinkoff-sdk-grpc-js ` เป็นสิ่งสำคัญที่นักพัฒนาซอฟต์แวร์จะอัปเดตแพ็คเกจเป็นเวอร์ชันล่าสุด ติดตั้ง
npm ฉันtinkoff-sdk-grpc-js
กำลังตรวจสอบ
const { createSdk } = ต้องการ (‘tinkoff-sdk-grpc-js’); // โทเค็นที่สามารถรับได้เช่นนี้ const TOKEN = ‘YOURAPI’; // ชื่อของแอปพลิเคชันที่คุณสามารถพบได้ในบันทึก TCS const appName = ‘tcsstat’; const sdk = createSdk (TOKEN, ชื่อแอป); (async () => { console.log(รอ sdk.users.getAccounts()); })();
ผลลัพธ์: รายการบัญชีของคุณจะแสดงในคอนโซล ตัวอย่างเช่น ลอง วิเคราะห์ความแตกต่าง:
- ในรายการบัญชีมี “ธนาคารเพื่อการลงทุน” ซึ่งคุณไม่สามารถใช้งาน API ได้
- โปรดทราบว่าฟิลด์เหล่านี้มาในรูปแบบ camelCase ขณะที่ในเอกสารประกอบ ฟิลด์เหล่านี้จะแสดงเป็น under_score
- จะเป็นแบบนี้ทุกที่ ดังนั้นคุณจึงไม่สามารถถ่ายและคัดลอกฟิลด์จากเอกสารประกอบได้
มีประโยชน์:
- คุณสามารถค้นหารหัสนี้ได้ในสาขาโครงการ
https://github.com/pskucherov/tcsstat/tree/step1 https://github.com/pskucherov/tcsstat/compare/step1
ดึงข้อมูลจาก Tinkoff Invest API ในเบราว์เซอร์
ฉันใช้ next.js และ socket.io นี่ไม่ใช่คำแนะนำที่แข็งแกร่ง โปรดเลือกตามดุลยพินิจของคุณ
npx create-next-app@latest npm ฉัน socket.io socket.io-client
เราจะไปที่ขั้นตอนมิตรภาพถัดไป+ซ็อกเก็ต+investapi ทันที และดู รายละเอียดทั้งหมดในส่วน ที่เป็นประโยชน์ของขั้นตอนนี้ ฉันจะอธิบายรายละเอียด:
- ที่ฝั่ง nodejs (เซิร์ฟเวอร์) จะมีไฟล์ page/api/investapi.js นี่คือที่ที่เราสร้างเซิร์ฟเวอร์ socket.io และเชื่อมต่อกับInvestapi
- ในด้านเบราว์เซอร์ (ไคลเอนต์) เราเชื่อมต่อกับเซิร์ฟเวอร์ผ่านซ็อกเก็ตและขอข้อมูลบัญชีจากนายหน้า
- เรารับข้อมูลจากนายหน้าบนเซิร์ฟเวอร์ จากนั้นส่งไปยังลูกค้า เมื่อได้รับบนไคลเอนต์ พวกเขาจะแสดงในเบราว์เซอร์
ผลลัพธ์: ในคอนโซลของเบราว์เซอร์ เราสามารถดูข้อมูลเกี่ยวกับบัญชีได้ นั่นคือ ในขั้นตอนสุดท้าย เราเห็นข้อมูลเกี่ยวกับบัญชีในคอนโซลเซิร์ฟเวอร์ (nodejs) ในขั้นตอนปัจจุบัน เราถ่ายโอนข้อมูลนี้ไปยังไคลเอ็นต์ (เบราว์เซอร์)
ตอนนี้มาทำให้คุณสามารถเลือกบัญชีจากเบราว์เซอร์และหากไม่มีโทเค็น ข้อผิดพลาดจะถูกส่งไปยังคอนโซล งานนั้นเรียบง่ายและไม่มีอะไรใหม่ ดังนั้นฉันจึงให้ลิงก์ไปยังคอมมิชชันเท่านั้น
- https://github.com/pskucherov/tcsstat/commit/7e1ac57061e5e971588479015b06d8814d6609a9
- https://github.com/pskucherov/tcsstat/commit/b28ac973a57494f5232589b4cb6b9fb13b8af759
มีประโยชน์:
- วิธีสร้างเพื่อนใหม่และซ็อกเก็ตมีรายละเอียดอธิบายไว้ที่ นี่
- รหัสมิตรภาพ ถัดไป+ซ็อกเก็ต+investapi:
https://github.com/pskucherov/tcsstat/commit/a443a4ac1bb4f0aa898f638128755fe7391ee381 สำหรับผู้ที่ยากข้างต้นเรายังคงอยู่ในขั้นตอนนี้และจัดการกับรหัส หากคุณมีคำถาม – ถาม https://github.com/pskucherov/tcsstat/tree/step2 https://github.com/pskucherov/tcsstat/compare/step1…step2
รับรายงานนายหน้าและธุรกรรม
มีสามวิธีในการรับรายงานและธุรกรรมการเป็นนายหน้าซื้อขายหลักทรัพย์
ตั้งแต่เริ่มต้นสิ่งสำคัญคือต้องรู้:
- รายงานนายหน้าถูกสร้างขึ้นในโหมด T-3 เช่น การซื้อขายจะแสดงที่นั่นหลังจากดำเนินการจริง
- ดังนั้น หากคุณขอรายงานนี้ในช่วงสองวันที่ผ่านมา รายงานจะพร้อมภายในสามวัน
- ในการรับธุรกรรมในวันสุดท้าย เราใช้วิธีการรับธุรกรรม แต่โปรดจำไว้ว่ารหัสและเนื้อหาอาจเปลี่ยนแปลงหลังจากสร้างรายงานนายหน้าซื้อขายหลักทรัพย์
GetBrokerReport
ในการรับรายงานนายหน้า คุณต้องใช้รหัสบัญชี วันที่เริ่มต้นและวันที่สิ้นสุดของรายงาน แต่ไม่เกิน 31 วัน เราส่งคำขอเพื่อสร้างรายงานไปยัง API ในการสร้าง_broker_report_request รับรหัสงานตอบกลับ หลังจากนั้น เมื่อใช้รหัสงานนี้ เราได้รับข้อมูลจากget _broker_report_response
- คุณต้องบันทึก TaskID ตลอดไปสำหรับวันที่เหล่านี้
- เนื่องจากหากคุณทำหาย สำหรับวันที่ร้องขอ รายงานจะตอบสนองคำขอการสร้างก่อน
- แล้วมันจะไม่มาเลย
[/สปอยล์] เรามาเริ่มเขียนโค้ดกันเลย
วิธีการรับวันที่โดยคำนึงถึงการลบจากวันที่ปัจจุบัน
const getDateSubDay = (subDay = 5, start = true) => { วันที่ const = วันที่ใหม่ (); date.setUTCDate(date.getUTCDate() – วันย่อย); ถ้า (เริ่ม) { date.setUTCHours (0, 0, 0, 0); } อื่น { date.setUTCHours(23, 59, 59, 999); } วันที่กลับ; };
คำขอสร้างรายงาน
const BrokerReport = รอคอย (sdk.operations.getBrokerReport) ({ createBrokerReportRequest: { accountId, จาก, ถึง, }, });
ผลลัพธ์:
- ผลจากการดำเนินการคำสั่งครั้งแรก เราได้รับรหัสงาน
- รายงานจะเริ่มสร้างจากฝั่งของโบรกเกอร์ เมื่อไม่ทราบพร้อม เราจะรอและดึงรหัสงานเป็นระยะเพื่อรอรายงาน
- ทำไม เพราะหากรายงานไม่พร้อมก็จะเกิดข้อผิดพลาด หากรายงานไม่พร้อมสำหรับโบรกเกอร์ นี่เป็นข้อผิดพลาดในโค้ดของคุณ โปรดดำเนินการ: 30058|INVALID_ARGUMENT|งานยังไม่เสร็จสมบูรณ์ โปรดลองอีกครั้งในภายหลัง
รหัสสำหรับรอรับและรับรายงานมีลักษณะดังนี้
ตัวจับเวลา const = เวลา async => { ส่งคืนสัญญาใหม่ (แก้ไข => setTimeout (แก้ไข, เวลา)); } const getBrokerResponseByTaskId = async (taskId, page = 0) => { ลอง { ส่งคืนการรอคอย (sdk.operations.getBrokerReport) ({ getBrokerReportRequest: { taskId, page, }, }); } catch (e) { console.log(‘รอ’, e); รอตัวจับเวลา (10,000); กลับกำลังรอ getBrokerResponseByTaskId (taskId, page); } };
จากนั้นเวทมนตร์เดียวกันก็เกิดขึ้น เราหยุดสคริปต์ของเรา เริ่มใหม่อีกครั้ง เราไม่มีรหัสงาน เรารันโค้ดด้วยคำขอ taskId แต่เราไม่ได้รับ taskId อีกต่อไป แต่ได้รับรายงานทันที มายากล! และทุกอย่างคงจะดีถ้าเป็นแบบนี้เสมอ แต่ในหนึ่งเดือนจะไม่มีข้อมูลเลย มีประโยชน์:
https://github.com/pskucherov/tcsstat/tree/step3.1 https://github.com/pskucherov/tcsstat/compare/step3.1
- หากมีใครเจอสิ่งนี้ก็ยินดีต้อนรับสู่ปัญหา หลังจากที่พวกเขาซ่อมแซมเวทมนตร์นี้แล้ว มันจะสูญเสียพลังและจะแตกต่างออกไป แต่ในขณะนี้ (03/21/2023) มันใช้งานได้อย่างนั้น
รับเงินปันผลผู้ออกต่างประเทศ
บางคนอาจคิดว่าวิธีนี้คล้ายกับวิธีก่อนหน้าและคุณสามารถใช้วิธีเดียวที่คุณเปลี่ยนชื่อการดำเนินการเท่านั้น แต่พวกเขาเดาไม่ถูก! การตั้งชื่อนั้นแตกต่างกันมากทั้งในวิธีการและข้อมูลที่ส่งคืน และการนับหน้าเริ่มจาก 0 จากนั้นจาก 1 เพื่อไม่ให้สับสนในทั้งหมดนี้การเขียนสองวิธีจะง่ายกว่า ซึ่งก็แปลกเพราะว่า ตรรกะของการทำงานเหมือนกัน ฉันทะเลาะวิวาทกันเป็นเวลานานเมื่อพยายามสร้างวิธีหนึ่งและมีรหัสน้อยกว่า จะไม่มีตัวอย่างที่นี่
GetOperationsByCursor
ที่ชื่นชอบของฉันในสาม แม้จะไม่ถูกต้องที่สุด แต่ก็เพียงพอที่สุด เราส่งคำขอตั้งแต่เริ่มสร้างบัญชีจนถึงวันที่เป็นไปได้สูงสุด (ปิดบัญชีหรือปัจจุบัน) เราได้รับคำตอบ ใช้เคอร์เซอร์และขอใหม่ตราบเท่าที่มีข้อมูล และโค้ดก็กระชับกว่าในตัวอย่างด้านบน
ตัวจับเวลา const = เวลา async => { ส่งคืนสัญญาใหม่ (แก้ไข => setTimeout (แก้ไข, เวลา)); } const getOperationsByCursor = async (sdk, accountId, from, to, cursor = ”) => { ลอง { const reqData = { accountId, from, to, limit: 1,000, state: sdk.OperationState.OPERATION_STATE_EXECUTED, withoutCommissions: false, ไม่มีการซื้อขาย: เท็จ, ไม่มีค้างคืน: เท็จ, เคอร์เซอร์, }; ส่งคืนการรอ sdk.operations.getOperationsByCursor (reqData); } catch (e) { ตัวจับเวลารอ (60,000); กลับกำลังรอ getOperationsByCursor (sdk, accountId, จาก, ถึง, เคอร์เซอร์ = ”); } };
แบบร่างที่จะเรียกใช้อยู่ที่นี่: https://github.com/pskucherov/tcsstat/tree/step3.3 https://github.com/pskucherov/tcsstat/compare/step3.3 ตอนนี้เราพร้อมที่จะเพิ่มการดำเนินการรับ ใบสมัครของเรา หากดำเนินการอย่างถูกต้อง คุณจะต้องได้รับรายงานการเป็นนายหน้าสำหรับการมีอยู่ทั้งหมดของบัญชี และสำหรับข้อมูลที่ขาดหายไป T-3 เดิมเหล่านั้น จะโหลดซ้ำจากการดำเนินการ แต่สิ่งนี้สามารถแยกออกเป็นบทความแยกต่างหาก ความแตกต่างหลักๆ ที่คุณจะพบก็คือการเกาะติดการดำเนินงานและรายงานการเป็นนายหน้าซื้อขายหลักทรัพย์
- หากวันนี้คุณได้รับรายงานนายหน้าซื้อขายหลักทรัพย์และธุรกรรมตามวันที่กำหนด ให้ใส่ข้อมูลทั้งหมดลงในฐานข้อมูล ก็จะไม่มีปัญหา
- คุณจะมีปัญหาในวันพรุ่งนี้เมื่อคุณได้รับข้อมูลส่วนถัดไปจากรายงานและการดำเนินการ และตัดสินใจที่จะซิงโครไนซ์กับฐานข้อมูลที่มีอยู่
- ความแตกต่างมากมายเกี่ยวกับรหัสที่ไม่ตรงกันหรือการเปลี่ยนรหัสหลังการประมวลผล
- สำหรับตลาด OTC รหัสไม่ตรงกันเลย
- เช่นเดียวกับความแตกต่างของเครื่องมือซิงโครไนซ์ซึ่งไม่ตรงกันอีกครั้งเนื่องจากลักษณะเฉพาะของ API แต่นั่นเป็นอีกเรื่องหนึ่ง
มาเพิ่มการรับข้อมูลเกี่ยวกับการดำเนินงานในแอปพลิเคชันของเรา คำถามหลักคือที่ที่ข้อมูลจะถูกประมวลผลและจัดเก็บ
- หากคุณทำเพื่อตัวคุณเอง คุณจะใช้ข้อมูลเดียวกันจากอุปกรณ์ต่างๆ จากนั้นคุณต้องประมวลผลและจัดเก็บข้อมูลบนเซิร์ฟเวอร์
- หากคุณมีข้อมูลที่แตกต่างกันจำนวนมากที่ใช้โดยผู้ใช้หลายคน คุณต้องตัดสินใจว่าอะไรสำคัญกว่ากัน: ความเร็วของผู้ใช้หรือการประหยัดธาตุเหล็กในด้านของคุณ ใครก็ตามที่สามารถซื้อฮาร์ดแวร์จำนวนไม่จำกัดได้นับรวมทุกอย่างบนเซิร์ฟเวอร์ของเขา และทำให้เร็วเป็นพิเศษสำหรับผู้ใช้ ช่วยประหยัดทรัพยากรของผู้ใช้ เช่น แบตเตอรี่และทราฟฟิก ซึ่งเป็นสิ่งสำคัญมากสำหรับโทรศัพท์
ในทางกลับกัน การนับในเบราว์เซอร์ไม่ใช่วิธีแก้ปัญหาที่เหมาะสมที่สุดในหลักการ ดังนั้นสิ่งที่ไม่แพงเราจะพิจารณาบนเซิร์ฟเวอร์ของเรา เราปล่อยให้ส่วนที่เหลือเป็นของลูกค้า ฉันต้องการคำนวณค่าคอมมิชชั่นบนเซิร์ฟเวอร์จริงๆ แต่นี่คือความแตกต่างเล็กน้อยที่เรียกว่า “การโต้ตอบ” สมมติว่าคุณมีการดำเนินการหลายพันรายการและต้องใช้เวลาห้านาทีในการรับ ผู้ใช้จะได้อะไรในเวลานี้? ตัวหมุน? ความคืบหน้า? Infa เกี่ยวกับจำนวนเงินที่ถูกอัปโหลด? ควรใช้ “การรออย่างกระตือรือร้น” เมื่อผู้ใช้ในกระบวนการสามารถเห็นบางอย่างได้แล้ว นี่คือ ผลลัพธ์:
- กำลังโหลดหน้า
- ขอใบแจ้งหนี้ทั้งหมด
- หลังจากนั้น ธุรกรรมทั้งหมดที่มีค่าคอมมิชชั่นสำหรับธุรกรรมที่ดำเนินการจะถูกร้องขอสำหรับทุกบัญชี เมื่อได้รับข้อมูลแล้ว ข้อมูลจะถูกแสดงผลในเบราว์เซอร์
เพื่อไม่ให้กรองข้อมูลในเหตุการณ์แต่ละครั้ง เราจะดึงเหตุการณ์ของเราเองสำหรับแต่ละบัญชี แบบนี้:
socket.emit(‘sdk:getOperationsCommissionResult_’ + accountId, { รายการ: data?.items, inProgress: บูลีน (nextCursor), });
แบบร่างที่จะเปิดตัวอยู่ที่นี่: https://github.com/pskucherov/tcsstat/tree/step3 https://github.com/pskucherov/tcsstat/compare/step2…step3 ก้าวต่อไป เป็นเรื่องดีที่คุณได้อ่านบรรทัดนี้!
การคำนวณและการส่งออกข้อมูลที่น่าสนใจ
ขึ้นอยู่กับว่าใครต้องการข้อมูลอะไร ดังนั้นฉันจะบอกคุณทันทีถึงความแตกต่างหลักที่คุณจะพบ
ทำงานกับราคา
ทุกคนที่ทำงานกับการเงินรู้ดีว่าการทำธุรกรรมทางการเงินควรทำด้วยจำนวนเต็มเท่านั้น เนื่องจากความไม่ถูกต้องของค่าหลังจุดทศนิยมและข้อผิดพลาดสะสมที่มีการดำเนินการจำนวนมาก นั่นคือเหตุผลที่ราคาทั้งหมดแสดงใน รูปแบบ MoneyValue ต่อไปนี้
สนาม | พิมพ์ | คำอธิบาย |
---|---|---|
สกุลเงิน | สตริง | สตริงรหัสสกุลเงิน ISO |
หน่วย | int64 | ส่วนที่เป็นจำนวนเต็มของผลรวม สามารถเป็นจำนวนลบได้ |
นาโน | int32 | ส่วนที่เป็นเศษส่วนของจำนวนเงิน สามารถเป็นจำนวนลบได้ |
เราประมวลผลแยกจากกัน แล้วนำมาคิดราคาตามราคา:
quote.units + quote.nano / 1e9
ต้นทุนของสัญญาซื้อขายล่วงหน้า
ราคาของฟิวเจอร์สจะแสดงเป็นจุด เมื่อคุณมีฟิวเจอร์สสกุลเงิน คุณจำเป็นต้องรู้อัตรา และแน่นอนว่าราคาเป็นแต้มและขั้นราคา เมื่อคุณคำนวณกำไรจากธุรกรรม สิ่งนี้สามารถยิงได้เพราะ หากคุณคำนวณจำนวนเงินทั้งหมดโดยการคูณราคาด้วยปริมาณ ที่นี่คุณต้องระวัง สำหรับตอนนี้เราจะดูว่ามันเป็นอย่างไร สิ่งนี้ใช้กับฟิวเจอร์สของสกุลเงิน ที่อื่น ๆ ทุกอย่างโอเคกับสิ่งนี้
ตลาด อ.ต.ก
ตลาดนี้มีลักษณะเฉพาะมากมายดังนั้นเรามาศึกษาการดำเนินการแยกกัน เมื่อคุณเริ่มดำเนินการซิงโครไนซ์ปรากฎว่าคุณต้องนำ figi / ticker มาในรูปแบบเดียวกันเพื่อให้จับคู่เครื่องดนตรีได้อย่างถูกต้อง เมื่อคุณเริ่มซิงโครไนซ์ข้อมูลนี้กับรายงานนายหน้าซื้อขายหลักทรัพย์ ปรากฎว่า TradeID ของธุรกรรมเดียวกันมีตัวอักษรขึ้นต้นธุรกรรมและไม่ได้อยู่ในรายงานนายหน้าซื้อขายหลักทรัพย์ ดังนั้นจึงไม่สามารถเปรียบเทียบได้ … อะแฮ่มอะแฮ่ม … โดยการเปรียบเทียบ! ฉันจับคู่เวลาซื้อขาย ติ๊กเกอร์ และจับคู่ที่รหัสการค้าหนึ่งอยู่ในอีกรหัสการค้าหนึ่ง ใช่ฉันไม่รู้ ใครเจอแบบนี้ใครสนก็เข้าเรื่องหรือเริ่มต้นใหม่
การดำเนินการทางคณิตศาสตร์เกี่ยวกับเครื่องมือ
เป็นไปไม่ได้ที่จะดำเนินการทางคณิตศาสตร์กับรายการทั้งหมดโดยไม่ดู เพื่อไม่ให้เพิ่ม warm to soft เราจะตรวจสอบสกุลเงินเสมอและดำเนินการเฉพาะเมื่อเราแน่ใจว่าสกุลเงินนั้นตรงกัน และคะแนนจะถูกแปลงเป็นสกุลเงินที่ต้องการ ด้วยความรู้เกี่ยวกับการทำงานกับหมายเลขธนาคาร เราจะคำนวณค่าคอมมิชชั่นที่ใช้ในแต่ละบัญชี แบบนี้: https://github.com/pskucherov/tcsstat/tree/step4 https://github.com/pskucherov/tcsstat/compare/step3…step4
ไมโครเซอร์วิสพร้อมแล้ว!
https://github.com/pskucherov/tcsstat เพื่อเป็นการบ้าน คุณสามารถตรวจสอบว่าบริการทำงานด้วยการเชื่อมต่อที่ช้าหรือไม่ เมื่อการเชื่อมต่อขาดหาย เมื่ออินเทอร์เน็ตถูกตัดการเชื่อมต่อ เมื่อข้อผิดพลาดหรือขีดจำกัดหมดอายุในส่วนของโบรกเกอร์
ข้อสรุปและแผนสำหรับอนาคต
- เรียนรู้เกี่ยวกับการทำงานขั้นพื้นฐานและการทำงานร่วมกับ Invest API
- ใช้เวลา ~ 10 ชม
- ระดับความยาก ~ จูเนียร์+ / กลางต่ำ
หากคุณปรับแต่ง microservice ต่อไป คุณอาจเจออะไรแบบนี้
https://opexbot.info
นี่คือพัฒนาการของฉัน สำหรับผู้ที่ขี้เกียจเกินกว่าจะเข้าใจ วิ่งและพึ่งพาตนเอง ฉันวางแผนที่จะเพิ่มการวิเคราะห์ตามคำขอของผู้ใช้ หากคุณชอบบทความนี้ สมัครสมาชิกช่องโทรเลข ของฉัน .
Полезная статья. Не могу представить, сколько усилий автора потребовалось, чтобы все описать. Благодарю.